source: sasview/src/sas/models/resolution.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: 31.3 KB
Line 
1"""
2Define the resolution functions for the data.
3
4This defines classes for 1D and 2D resolution calculations.
5"""
6from __future__ import division
7from scipy.special import erf
8from numpy import sqrt, log, log10
9import numpy as np
10
11MINIMUM_RESOLUTION = 1e-8
12
13class Resolution1D(object):
14    """
15    Abstract base class defining a 1D resolution function.
16
17    *q* is the set of q values at which the data is measured.
18
19    *q_calc* is the set of q values at which the theory needs to be evaluated.
20    This may extend and interpolate the q values.
21
22    *apply* is the method to call with I(q_calc) to compute the resolution
23    smeared theory I(q).
24    """
25    q = None
26    q_calc = None
27    def apply(self, theory):
28        """
29        Smear *theory* by the resolution function, returning *Iq*.
30        """
31        raise NotImplementedError("Subclass does not define the apply function")
32
33
34class Perfect1D(Resolution1D):
35    """
36    Resolution function to use when there is no actual resolution smearing
37    to be applied.  It has the same interface as the other resolution
38    functions, but returns the identity function.
39    """
40    def __init__(self, q):
41        self.q_calc = self.q = q
42
43    def apply(self, theory):
44        return theory
45
46
47class Pinhole1D(Resolution1D):
48    r"""
49    Pinhole aperture with q-dependent gaussian resolution.
50
51    *q* points at which the data is measured.
52
53    *q_width* gaussian 1-sigma resolution at each data point.
54
55    *q_calc* is the list of points to calculate, or None if this should
56    be estimated from the *q* and *q_width*.
57    """
58    def __init__(self, q, q_width, q_calc=None, nsigma=3):
59        #*min_step* is the minimum point spacing to use when computing the
60        #underlying model.  It should be on the order of
61        #$\tfrac{1}{10}\tfrac{2\pi}{d_\text{max}}$ to make sure that fringes
62        #are computed with sufficient density to avoid aliasing effects.
63
64        # Protect against calls with q_width=0.  The extend_q function will
65        # not extend the q if q_width is 0, but q_width must be non-zero when
66        # constructing the weight matrix to avoid division by zero errors.
67        # In practice this should never be needed, since resolution should
68        # default to Perfect1D if the pinhole geometry is not defined.
69        self.q, self.q_width = q, q_width
70        self.q_calc = pinhole_extend_q(q, q_width, nsigma=nsigma) \
71            if q_calc is None else np.sort(q_calc)
72        self.weight_matrix = pinhole_resolution(self.q_calc,
73                self.q, np.maximum(q_width, MINIMUM_RESOLUTION))
74
75    def apply(self, theory):
76        return apply_resolution_matrix(self.weight_matrix, theory)
77
78
79class Slit1D(Resolution1D):
80    """
81    Slit aperture with a complicated resolution function.
82
83    *q* points at which the data is measured.
84
85    *qx_width* slit width
86
87    *qy_height* slit height
88
89    *q_calc* is the list of points to calculate, or None if this should
90    be estimated from the *q* and *q_width*.
91
92    The *weight_matrix* is computed by :func:`slit1d_resolution`
93    """
94    def __init__(self, q, width, height, q_calc=None):
95        # TODO: maybe issue warnings rather than raising errors
96        if not np.isscalar(width):
97            if np.any(np.diff(width) > 0.0):
98                raise ValueError("Slit resolution requires fixed width slits")
99            width = width[0]
100        if not np.isscalar(height):
101            if np.any(np.diff(height) > 0.0):
102                raise ValueError("Slit resolution requires fixed height slits")
103            height = height[0]
104
105        # Remember what width/height was used even though we won't need them
106        # after the weight matrix is constructed
107        self.width, self.height = width, height
108
109        self.q = q.flatten()
110        self.q_calc = slit_extend_q(q, width, height) \
111            if q_calc is None else np.sort(q_calc)
112        self.weight_matrix = \
113            slit_resolution(self.q_calc, self.q, width, height)
114
115    def apply(self, theory):
116        return apply_resolution_matrix(self.weight_matrix, theory)
117
118
119def apply_resolution_matrix(weight_matrix, theory):
120    """
121    Apply the resolution weight matrix to the computed theory function.
122    """
123    #print "apply shapes", theory.shape, weight_matrix.shape
124    Iq = np.dot(theory[None,:], weight_matrix)
125    #print "result shape",Iq.shape
126    return Iq.flatten()
127
128
129def pinhole_resolution(q_calc, q, q_width):
130    """
131    Compute the convolution matrix *W* for pinhole resolution 1-D data.
132
133    Each row *W[i]* determines the normalized weight that the corresponding
134    points *q_calc* contribute to the resolution smeared point *q[i]*.  Given
135    *W*, the resolution smearing can be computed using *dot(W,q)*.
136
137    *q_calc* must be increasing.  *q_width* must be greater than zero.
138    """
139    # The current algorithm is a midpoint rectangle rule.  In the test case,
140    # neither trapezoid nor Simpson's rule improved the accuracy.
141    edges = bin_edges(q_calc)
142    edges[edges<0.0] = 0.0 # clip edges below zero
143    G = erf( (edges[:,None] - q[None,:]) / (sqrt(2.0)*q_width)[None,:] )
144    weights = G[1:] - G[:-1]
145    weights /= np.sum(weights, axis=0)[None,:]
146    return weights
147
148
149def slit_resolution(q_calc, q, width, height):
150    r"""
151    Build a weight matrix to compute *I_s(q)* from *I(q_calc)*, given
152    $q_v$ = *width* and $q_h$ = *height*.
153
154    *width* and *height* are scalars since current instruments use the
155    same slit settings for all measurement points.
156
157    If slit height is large relative to width, use:
158
159    .. math::
160
161        I_s(q_o) = \frac{1}{\Delta q_v}
162            \int_0^{\Delta q_v} I(\sqrt{q_o^2 + u^2} du
163
164    If slit width is large relative to height, use:
165
166    .. math::
167
168        I_s(q_o) = \frac{1}{2 \Delta q_v}
169            \int_{-\Delta q_v}^{\Delta q_v} I(u) du
170    """
171    if width == 0.0 and height == 0.0:
172        #print "condition zero"
173        return 1
174
175    q_edges = bin_edges(q_calc) # Note: requires q > 0
176    q_edges[q_edges<0.0] = 0.0 # clip edges below zero
177
178    #np.set_printoptions(linewidth=10000)
179    if width <= 100.0 * height or height == 0:
180        # The current algorithm is a midpoint rectangle rule.  In the test case,
181        # neither trapezoid nor Simpson's rule improved the accuracy.
182        #print "condition h", q_edges.shape, q.shape, q_calc.shape
183        weights = np.zeros((len(q), len(q_calc)), 'd')
184        for i, qi in enumerate(q):
185            weights[i, :] = np.diff(q_to_u(q_edges, qi))
186        weights /= width
187        weights = weights.T
188    else:
189        #print "condition w"
190        # Make q_calc into a row vector, and q into a column vector
191        q, q_calc = q[None,:], q_calc[:,None]
192        in_x = (q_calc >= q-width) * (q_calc <= q+width)
193        weights = np.diff(q_edges)[:,None] * in_x
194
195    return weights
196
197
198def pinhole_extend_q(q, q_width, nsigma=3):
199    """
200    Given *q* and *q_width*, find a set of sampling points *q_calc* so
201    that each point I(q) has sufficient support from the underlying
202    function.
203    """
204    q_min, q_max = np.min(q - nsigma*q_width), np.max(q + nsigma*q_width)
205    return linear_extrapolation(q, q_min, q_max)
206
207
208def slit_extend_q(q, width, height):
209    """
210    Given *q*, *width* and *height*, find a set of sampling points *q_calc* so
211    that each point I(q) has sufficient support from the underlying
212    function.
213    """
214    height # keep lint happy
215    q_min, q_max = np.min(q), np.max(np.sqrt(q**2 + width**2))
216    return geometric_extrapolation(q, q_min, q_max)
217
218
219def bin_edges(x):
220    """
221    Determine bin edges from bin centers, assuming that edges are centered
222    between the bins.
223
224    Note: this uses the arithmetic mean, which may not be appropriate for
225    log-scaled data.
226    """
227    if len(x) < 2 or (np.diff(x)<0).any():
228        raise ValueError("Expected bins to be an increasing set")
229    edges = np.hstack([
230        x[0]  - 0.5*(x[1]  - x[0]),  # first point minus half first interval
231        0.5*(x[1:] + x[:-1]),        # mid points of all central intervals
232        x[-1] + 0.5*(x[-1] - x[-2]), # last point plus half last interval
233        ])
234    return edges
235
236
237def q_to_u(q, q0):
238    """
239    Convert *q* values to *u* values for the integral computed at *q0*.
240    """
241    # array([value])**2 - value**2 is not always zero
242    qpsq = q**2 - q0**2
243    qpsq[qpsq<0] = 0
244    return sqrt(qpsq)
245
246
247def interpolate(q, max_step):
248    """
249    Returns *q_calc* with points spaced at most max_step apart.
250    """
251    step = np.diff(q)
252    index = step>max_step
253    if np.any(index):
254        inserts = []
255        for q_i,step_i in zip(q[:-1][index],step[index]):
256            n = np.ceil(step_i/max_step)
257            inserts.extend(q_i + np.arange(1,n)*(step_i/n))
258        # Extend a couple of fringes beyond the end of the data
259        inserts.extend(q[-1] + np.arange(1,8)*max_step)
260        q_calc = np.sort(np.hstack((q,inserts)))
261    else:
262        q_calc = q
263    return q_calc
264
265
266def linear_extrapolation(q, q_min, q_max):
267    """
268    Extrapolate *q* out to [*q_min*, *q_max*] using the step size in *q* as
269    a guide.  Extrapolation below uses about the same size as the first
270    interval.  Extrapolation above uses about the same size as the final
271    interval.
272
273    if *q_min* is zero or less then *q[0]/10* is used instead.
274    """
275    q = np.sort(q)
276    if q_min < q[0]:
277        if q_min <= 0: q_min = q[0]/10
278        n_low = np.ceil((q[0]-q_min) / (q[1]-q[0])) if q[1]>q[0] else 15
279        q_low = np.linspace(q_min, q[0], n_low+1)[:-1]
280    else:
281        q_low = []
282    if q_max > q[-1]:
283        n_high = np.ceil((q_max-q[-1]) / (q[-1]-q[-2])) if q[-1]>q[-2] else 15
284        q_high = np.linspace(q[-1], q_max, n_high+1)[1:]
285    else:
286        q_high = []
287    return np.concatenate([q_low, q, q_high])
288
289
290def geometric_extrapolation(q, q_min, q_max, points_per_decade=None):
291    r"""
292    Extrapolate *q* to [*q_min*, *q_max*] using geometric steps, with the
293    average geometric step size in *q* as the step size.
294
295    if *q_min* is zero or less then *q[0]/10* is used instead.
296
297    *points_per_decade* sets the ratio between consecutive steps such
298    that there will be $n$ points used for every factor of 10 increase
299    in *q*.
300
301    If *points_per_decade* is not given, it will be estimated as follows.
302    Starting at $q_1$ and stepping geometrically by $\Delta q$ to $q_n$
303    in $n$ points gives a geometric average of:
304
305    .. math::
306
307         \log \Delta q = (\log q_n - log q_1) / (n - 1)
308
309    From this we can compute the number of steps required to extend $q$
310    from $q_n$ to $q_\text{max}$ by $\Delta q$ as:
311
312    .. math::
313
314         n_\text{extend} = (\log q_\text{max} - \log q_n) / \log \Delta q
315
316    Substituting:
317
318        n_\text{extend} = (n-1) (\log q_\text{max} - \log q_n)
319            / (\log q_n - log q_1)
320    """
321    q = np.sort(q)
322    if points_per_decade is None:
323        log_delta_q = (len(q) - 1) / (log(q[-1]) - log(q[0]))
324    else:
325        log_delta_q = log(10.) / points_per_decade
326    if q_min < q[0]:
327        if q_min < 0: q_min = q[0]/10
328        n_low = log_delta_q * (log(q[0])-log(q_min))
329        q_low  = np.logspace(log10(q_min), log10(q[0]), np.ceil(n_low)+1)[:-1]
330    else:
331        q_low = []
332    if q_max > q[-1]:
333        n_high = log_delta_q * (log(q_max)-log(q[-1]))
334        q_high = np.logspace(log10(q[-1]), log10(q_max), np.ceil(n_high)+1)[1:]
335    else:
336        q_high = []
337    return np.concatenate([q_low, q, q_high])
338
339
340############################################################################
341# unit tests
342############################################################################
343import unittest
344
345
346def eval_form(q, form, pars):
347    from sasmodels import core
348    kernel = core.make_kernel(form, [q])
349    theory = core.call_kernel(kernel, pars)
350    kernel.release()
351    return theory
352
353
354def gaussian(q, q0, dq):
355    from numpy import exp, pi
356    return exp(-0.5*((q-q0)/dq)**2)/(sqrt(2*pi)*dq)
357
358
359def romberg_slit_1d(q, delta_qv, form, pars):
360    """
361    Romberg integration for slit resolution.
362
363    This is an adaptive integration technique.  It is called with settings
364    that make it slow to evaluate but give it good accuracy.
365    """
366    from scipy.integrate import romberg
367
368    if any(k not in form.info['defaults'] for k in pars.keys()):
369        keys = set(form.info['defaults'].keys())
370        extra = set(pars.keys()) - keys
371        raise ValueError("bad parameters: [%s] not in [%s]"%
372                         (", ".join(sorted(extra)), ", ".join(sorted(keys))))
373
374    _fn = lambda u, q0: eval_form(sqrt(q0**2 + u**2), form, pars)
375    r = [romberg(_fn, 0, delta_qv[0], args=(qi,),
376                 divmax=100, vec_func=True, tol=0, rtol=1e-8)
377         for qi in q]
378    # r should be [float, ...], but it is [array([float]), array([float]),...]
379    return np.asarray(r).flatten()/delta_qv[0]
380
381
382def romberg_pinhole_1d(q, q_width, form, pars, nsigma=5):
383    """
384    Romberg integration for pinhole resolution.
385
386    This is an adaptive integration technique.  It is called with settings
387    that make it slow to evaluate but give it good accuracy.
388    """
389    from scipy.integrate import romberg
390
391    if any(k not in form.info['defaults'] for k in pars.keys()):
392        keys = set(form.info['defaults'].keys())
393        extra = set(pars.keys()) - keys
394        raise ValueError("bad parameters: [%s] not in [%s]"%
395                         (", ".join(sorted(extra)), ", ".join(sorted(keys))))
396
397    _fn = lambda q, q0, dq: eval_form(q, form, pars)*gaussian(q, q0, dq)
398    r = [romberg(_fn, max(qi-nsigma*dqi,1e-10*q[0]), qi+nsigma*dqi, args=(qi, dqi),
399                 divmax=100, vec_func=True, tol=0, rtol=1e-8)
400         for qi,dqi in zip(q,q_width)]
401    return np.asarray(r).flatten()
402
403
404class ResolutionTest(unittest.TestCase):
405
406    def setUp(self):
407        self.x = 0.001*np.arange(1, 11)
408        self.y = self.Iq(self.x)
409
410    def Iq(self, q):
411        "Linear function for resolution unit test"
412        return 12.0 - 1000.0*q
413
414    def test_perfect(self):
415        """
416        Perfect resolution and no smearing.
417        """
418        resolution = Perfect1D(self.x)
419        theory = self.Iq(resolution.q_calc)
420        output = resolution.apply(theory)
421        np.testing.assert_equal(output, self.y)
422
423    def test_slit_zero(self):
424        """
425        Slit smearing with perfect resolution.
426        """
427        resolution = Slit1D(self.x, width=0, height=0, q_calc=self.x)
428        theory = self.Iq(resolution.q_calc)
429        output = resolution.apply(theory)
430        np.testing.assert_equal(output, self.y)
431
432    @unittest.skip("not yet supported")
433    def test_slit_high(self):
434        """
435        Slit smearing with height 0.005
436        """
437        resolution = Slit1D(self.x, width=0, height=0.005, q_calc=self.x)
438        theory = self.Iq(resolution.q_calc)
439        output = resolution.apply(theory)
440        answer = [ 9.0618, 8.6402, 8.1187, 7.1392, 6.1528,
441                   5.5555, 4.5584, 3.5606, 2.5623, 2.0000 ]
442        np.testing.assert_allclose(output, answer, atol=1e-4)
443
444    @unittest.skip("not yet supported")
445    def test_slit_both_high(self):
446        """
447        Slit smearing with width < 100*height.
448        """
449        q = np.logspace(-4, -1, 10)
450        resolution = Slit1D(q, width=0.2, height=np.inf)
451        theory = 1000*self.Iq(resolution.q_calc**4)
452        output = resolution.apply(theory)
453        answer = [ 8.85785, 8.43012, 7.92687, 6.94566, 6.03660,
454                   5.40363, 4.40655, 3.40880, 2.41058, 2.00000 ]
455        np.testing.assert_allclose(output, answer, atol=1e-4)
456
457    @unittest.skip("not yet supported")
458    def test_slit_wide(self):
459        """
460        Slit smearing with width 0.0002
461        """
462        resolution = Slit1D(self.x, width=0.0002, height=0, q_calc=self.x)
463        theory = self.Iq(resolution.q_calc)
464        output = resolution.apply(theory)
465        answer = [ 11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0 ]
466        np.testing.assert_allclose(output, answer, atol=1e-4)
467
468    @unittest.skip("not yet supported")
469    def test_slit_both_wide(self):
470        """
471        Slit smearing with width > 100*height.
472        """
473        resolution = Slit1D(self.x, width=0.0002, height=0.000001,
474                            q_calc=self.x)
475        theory = self.Iq(resolution.q_calc)
476        output = resolution.apply(theory)
477        answer = [ 11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0 ]
478        np.testing.assert_allclose(output, answer, atol=1e-4)
479
480    def test_pinhole_zero(self):
481        """
482        Pinhole smearing with perfect resolution
483        """
484        resolution = Pinhole1D(self.x, 0.0*self.x)
485        theory = self.Iq(resolution.q_calc)
486        output = resolution.apply(theory)
487        np.testing.assert_equal(output, self.y)
488
489    def test_pinhole(self):
490        """
491        Pinhole smearing with dQ = 0.001 [Note: not dQ/Q = 0.001]
492        """
493        resolution = Pinhole1D(self.x, 0.001*np.ones_like(self.x),
494                               q_calc=self.x)
495        theory = 12.0-1000.0*resolution.q_calc
496        output = resolution.apply(theory)
497        answer = [ 10.44785079, 9.84991299, 8.98101708,
498                  7.99906585, 6.99998311, 6.00001689,
499                  5.00093415, 4.01898292, 3.15008701, 2.55214921]
500        np.testing.assert_allclose(output, answer, atol=1e-8)
501
502
503class IgorComparisonTest(unittest.TestCase):
504
505    def setUp(self):
506        self.pars = TEST_PARS_PINHOLE_SPHERE
507        from sasmodels import core
508        from sasmodels.models import sphere
509        self.model = core.load_model(sphere, dtype='double')
510
511    def Iq_sphere(self, pars, resolution):
512        from sasmodels import core
513        kernel = core.make_kernel(self.model, [resolution.q_calc])
514        theory = core.call_kernel(kernel, pars)
515        result = resolution.apply(theory)
516        kernel.release()
517        return result
518
519    def compare(self, q, output, answer, tolerance):
520        err = (output - answer)/answer
521        idx = abs(err) >= tolerance
522        problem = zip(q[idx], output[idx], answer[idx], err[idx])
523        print "\n".join(str(v) for v in problem)
524        np.testing.assert_allclose(output, answer, rtol=tolerance)
525
526    def test_perfect(self):
527        """
528        Compare sphere model with NIST Igor SANS, no resolution smearing.
529        """
530        pars = TEST_PARS_SLIT_SPHERE
531        data_string = TEST_DATA_SLIT_SPHERE
532
533        data = np.loadtxt(data_string.split('\n')).T
534        q, width, answer, _ = data
535        resolution = Perfect1D(q)
536        output = self.Iq_sphere(pars, resolution)
537        self.compare(q, output, answer, 1e-6)
538
539    def test_pinhole(self):
540        """
541        Compare pinhole resolution smearing with NIST Igor SANS
542        """
543        pars = TEST_PARS_PINHOLE_SPHERE
544        data_string = TEST_DATA_PINHOLE_SPHERE
545
546        data = np.loadtxt(data_string.split('\n')).T
547        q, q_width, answer = data
548        resolution = Pinhole1D(q, q_width)
549        output = self.Iq_sphere(pars, resolution)
550        # TODO: relative error should be lower
551        self.compare(q, output, answer, 3e-4)
552
553    def test_pinhole_romberg(self):
554        """
555        Compare pinhole resolution smearing with romberg integration result.
556        """
557        pars = TEST_PARS_PINHOLE_SPHERE
558        data_string = TEST_DATA_PINHOLE_SPHERE
559        pars['radius'] *= 5
560        radius = pars['radius']
561
562        data = np.loadtxt(data_string.split('\n')).T
563        q, q_width, answer = data
564        answer = romberg_pinhole_1d(q, q_width, self.model, pars)
565        ## Getting 0.1% requires 5 sigma and 200 points per fringe
566        #q_calc = interpolate(pinhole_extend_q(q, q_width, nsigma=5),
567        #                     2*np.pi/radius/200)
568        #tol = 0.001
569        ## The default 3 sigma and no extra points gets 1%
570        q_calc, tol = None, 0.01
571        resolution = Pinhole1D(q, q_width, q_calc=q_calc)
572        output = self.Iq_sphere(pars, resolution)
573        if 0: # debug plot
574            import matplotlib.pyplot as plt
575            resolution = Perfect1D(q)
576            source = self.Iq_sphere(pars, resolution)
577            plt.loglog(q, source, '.')
578            plt.loglog(q, answer, '-', hold=True)
579            plt.loglog(q, output, '-', hold=True)
580            plt.show()
581        self.compare(q, output, answer, tol)
582
583    def test_slit(self):
584        """
585        Compare slit resolution smearing with NIST Igor SANS
586        """
587        pars = TEST_PARS_SLIT_SPHERE
588        data_string = TEST_DATA_SLIT_SPHERE
589
590        data = np.loadtxt(data_string.split('\n')).T
591        q, delta_qv, _, answer = data
592        resolution = Slit1D(q, width=delta_qv, height=0)
593        output = self.Iq_sphere(pars, resolution)
594        # TODO: eliminate Igor test since it is too inaccurate to be useful.
595        # This means we can eliminate the test data as well, and instead
596        # use a generated q vector.
597        self.compare(q, output, answer, 0.5)
598
599    def test_slit_romberg(self):
600        """
601        Compare slit resolution smearing with romberg integration result.
602        """
603        pars = TEST_PARS_SLIT_SPHERE
604        data_string = TEST_DATA_SLIT_SPHERE
605        radius = pars['radius']
606
607        data = np.loadtxt(data_string.split('\n')).T
608        q, delta_qv, _, answer = data
609        answer = romberg_slit_1d(q, delta_qv, self.model, pars)
610        q_calc = slit_extend_q(interpolate(q, 2*np.pi/radius/20),
611                               delta_qv[0], delta_qv[0])
612        resolution = Slit1D(q, width=delta_qv, height=0, q_calc=q_calc)
613        output = self.Iq_sphere(pars, resolution)
614        # TODO: relative error should be lower
615        self.compare(q, output, answer, 0.025)
616
617    def test_ellipsoid(self):
618        """
619        Compare romberg integration for ellipsoid model.
620        """
621        from .core import load_model
622        pars = {
623            'scale':0.05,
624            'rpolar':500, 'requatorial':15000,
625            'sld':6, 'solvent_sld': 1,
626            }
627        form = load_model('ellipsoid', dtype='double')
628        q = np.logspace(log10(4e-5),log10(2.5e-2), 68)
629        delta_qv = [0.117]
630        resolution = Slit1D(q, width=delta_qv, height=0)
631        answer = romberg_slit_1d(q, delta_qv, form, pars)
632        output = resolution.apply(eval_form(resolution.q_calc, form, pars))
633        # TODO: 10% is too much error; use better algorithm
634        self.compare(q, output, answer, 0.1)
635
636    #TODO: can sas q spacing be too sparse for the resolution calculation?
637    @unittest.skip("suppress sparse data test; not supported by current code")
638    def test_pinhole_sparse(self):
639        """
640        Compare pinhole resolution smearing with NIST Igor SANS on sparse data
641        """
642        pars = TEST_PARS_PINHOLE_SPHERE
643        data_string = TEST_DATA_PINHOLE_SPHERE
644
645        data = np.loadtxt(data_string.split('\n')).T
646        q, q_width, answer = data[:, ::20] # Take every nth point
647        resolution = Pinhole1D(q, q_width)
648        output = self.Iq_sphere(pars, resolution)
649        self.compare(q, output, answer, 1e-6)
650
651
652# pinhole sphere parameters
653TEST_PARS_PINHOLE_SPHERE = {
654    'scale': 1.0, 'background': 0.01,
655    'radius': 60.0, 'sld': 1, 'solvent_sld': 6.3,
656    }
657# Q, dQ, I(Q) calculated by NIST Igor SANS package
658TEST_DATA_PINHOLE_SPHERE = """\
6590.001278 0.0002847 2538.41176383
6600.001562 0.0002905 2536.91820405
6610.001846 0.0002956 2535.13182479
6620.002130 0.0003017 2533.06217813
6630.002414 0.0003087 2530.70378586
6640.002698 0.0003165 2528.05024192
6650.002982 0.0003249 2525.10408349
6660.003266 0.0003340 2521.86667499
6670.003550 0.0003437 2518.33907750
6680.003834 0.0003539 2514.52246995
6690.004118 0.0003646 2510.41798319
6700.004402 0.0003757 2506.02690988
6710.004686 0.0003872 2501.35067884
6720.004970 0.0003990 2496.38678318
6730.005253 0.0004112 2491.16237596
6740.005537 0.0004237 2485.63911673
6750.005821 0.0004365 2479.83657083
6760.006105 0.0004495 2473.75676948
6770.006389 0.0004628 2467.40145990
6780.006673 0.0004762 2460.77293372
6790.006957 0.0004899 2453.86724627
6800.007241 0.0005037 2446.69623838
6810.007525 0.0005177 2439.25775219
6820.007809 0.0005318 2431.55421398
6830.008093 0.0005461 2423.58785521
6840.008377 0.0005605 2415.36158137
6850.008661 0.0005750 2406.87009473
6860.008945 0.0005896 2398.12841186
6870.009229 0.0006044 2389.13360806
6880.009513 0.0006192 2379.88958042
6890.009797 0.0006341 2370.39776774
6900.010080 0.0006491 2360.69528793
6910.010360 0.0006641 2350.85169027
6920.010650 0.0006793 2340.42023633
6930.010930 0.0006945 2330.11206013
6940.011220 0.0007097 2319.20109972
6950.011500 0.0007251 2308.43503981
6960.011780 0.0007404 2297.44820179
6970.012070 0.0007558 2285.83853677
6980.012350 0.0007713 2274.41290746
6990.012640 0.0007868 2262.36219581
7000.012920 0.0008024 2250.51169731
7010.013200 0.0008180 2238.45596231
7020.013490 0.0008336 2225.76495666
7030.013770 0.0008493 2213.29618391
7040.014060 0.0008650 2200.19110751
7050.014340 0.0008807 2187.34050325
7060.014620 0.0008965 2174.30529864
7070.014910 0.0009123 2160.61632548
7080.015190 0.0009281 2147.21038112
7090.015470 0.0009440 2133.62023580
7100.015760 0.0009598 2119.37907426
7110.016040 0.0009757 2105.45234903
7120.016330 0.0009916 2090.86319102
7130.016610 0.0010080 2076.60576032
7140.016890 0.0010240 2062.19214565
7150.017180 0.0010390 2047.10550219
7160.017460 0.0010550 2032.38715621
7170.017740 0.0010710 2017.52560123
7180.018030 0.0010880 2001.99124318
7190.018310 0.0011040 1986.84662060
7200.018600 0.0011200 1971.03389745
7210.018880 0.0011360 1955.61395119
7220.019160 0.0011520 1940.08291563
7230.019450 0.0011680 1923.87672225
7240.019730 0.0011840 1908.10656374
7250.020020 0.0012000 1891.66297192
7260.020300 0.0012160 1875.66789021
7270.020580 0.0012320 1859.56357196
7280.020870 0.0012490 1842.79468290
7290.021150 0.0012650 1826.50064489
7300.021430 0.0012810 1810.11533702
7310.021720 0.0012970 1793.06840882
7320.022000 0.0013130 1776.51153580
7330.022280 0.0013290 1759.87201249
7340.022570 0.0013460 1742.57354412
7350.022850 0.0013620 1725.79397319
7360.023140 0.0013780 1708.35831550
7370.023420 0.0013940 1691.45256069
7380.023700 0.0014110 1674.48561783
7390.023990 0.0014270 1656.86525366
7400.024270 0.0014430 1639.79847285
7410.024550 0.0014590 1622.68887088
7420.024840 0.0014760 1604.96421100
7430.025120 0.0014920 1587.85768129
7440.025410 0.0015080 1569.99297335
7450.025690 0.0015240 1552.84580279
7460.025970 0.0015410 1535.54074115
7470.026260 0.0015570 1517.75249337
7480.026540 0.0015730 1500.40115023
7490.026820 0.0015900 1483.03632237
7500.027110 0.0016060 1465.05942429
7510.027390 0.0016220 1447.67682181
7520.027670 0.0016390 1430.46495191
7530.027960 0.0016550 1412.49232282
7540.028240 0.0016710 1395.13182318
7550.028520 0.0016880 1377.93439837
7560.028810 0.0017040 1359.99528971
7570.029090 0.0017200 1342.67274512
7580.029370 0.0017370 1325.55375609
759"""
760
761# Slit sphere parameters
762TEST_PARS_SLIT_SPHERE = {
763    'scale': 0.01, 'background': 0.01,
764    'radius': 60000, 'sld': 1, 'solvent_sld': 4,
765    }
766# Q dQ I(Q) I_smeared(Q)
767TEST_DATA_SLIT_SPHERE = """\
7682.26097e-05 0.117 5.5781372896e+09 1.4626077708e+06
7692.53847e-05 0.117 5.0363141458e+09 1.3117318023e+06
7702.81597e-05 0.117 4.4850108103e+09 1.1594863713e+06
7713.09347e-05 0.117 3.9364658459e+09 1.0094881630e+06
7723.37097e-05 0.117 3.4019975074e+09 8.6518941303e+05
7733.92597e-05 0.117 2.4139519814e+09 6.0232158311e+05
7744.48097e-05 0.117 1.5816877820e+09 3.8739994090e+05
7755.03597e-05 0.117 9.3715407224e+08 2.2745304775e+05
7765.59097e-05 0.117 4.8387917428e+08 1.2101295768e+05
7776.14597e-05 0.117 2.0193586928e+08 6.0055107771e+04
7786.70097e-05 0.117 5.5886110911e+07 3.2749521065e+04
7797.25597e-05 0.117 3.7782348010e+06 2.6350963616e+04
7807.81097e-05 0.117 5.3407817904e+06 2.9624963314e+04
7818.36597e-05 0.117 2.7975485523e+07 3.4403962254e+04
7828.92097e-05 0.117 4.9845448282e+07 3.6130017903e+04
7839.47597e-05 0.117 6.0092588905e+07 3.3495107849e+04
7841.00310e-04 0.117 5.6823430831e+07 2.7475726279e+04
7851.05860e-04 0.117 4.3857024036e+07 2.0144282226e+04
7861.11410e-04 0.117 2.7277144760e+07 1.3647403260e+04
7871.22510e-04 0.117 3.3119334113e+06 6.6519711526e+03
7881.33610e-04 0.117 1.4412859402e+06 6.9726212813e+03
7891.44710e-04 0.117 8.5620162463e+06 8.1441335775e+03
7901.55810e-04 0.117 9.6957429033e+06 6.4559996521e+03
7911.66910e-04 0.117 4.3818341914e+06 3.6252154156e+03
7921.78010e-04 0.117 2.7448997387e+05 2.4006505342e+03
7931.89110e-04 0.117 8.0472009936e+05 2.8187789089e+03
7942.00210e-04 0.117 2.8149552834e+06 3.0915662855e+03
7952.11310e-04 0.117 2.7510907861e+06 2.3722530293e+03
7962.22410e-04 0.117 1.0053133293e+06 1.4473468311e+03
7972.33510e-04 0.117 5.8428305052e+03 1.2048540556e+03
7982.44610e-04 0.117 5.1699305004e+05 1.4625670042e+03
7992.55710e-04 0.117 1.2120227268e+06 1.5010705973e+03
8002.66810e-04 0.117 9.7896842846e+05 1.1336343426e+03
8012.77910e-04 0.117 2.5507264791e+05 8.1848818080e+02
8023.05660e-04 0.117 5.2403101181e+05 7.4913374357e+02
8033.33410e-04 0.117 5.8699343809e+04 4.4669964560e+02
8043.61160e-04 0.117 3.0844327150e+05 4.6774007542e+02
8053.88910e-04 0.117 8.3360142970e+03 2.7169550220e+02
8064.16660e-04 0.117 1.8630080583e+05 3.0710983679e+02
8074.44410e-04 0.117 3.1616804732e-01 1.7959006831e+02
8084.72160e-04 0.117 1.1299016314e+05 2.0763952339e+02
8094.99910e-04 0.117 2.9952522747e+03 1.2536542765e+02
8105.27660e-04 0.117 6.7625695649e+04 1.4013969777e+02
8115.55410e-04 0.117 7.6927460089e+03 8.2145593180e+01
8126.10910e-04 0.117 1.1229057779e+04 8.4519745643e+01
8136.66410e-04 0.117 1.3035567943e+04 8.1554625609e+01
8147.21910e-04 0.117 1.3309931343e+04 7.4437319172e+01
8157.77410e-04 0.117 1.2462626212e+04 6.4697088261e+01
8168.32910e-04 0.117 1.0912927143e+04 5.3773301044e+01
8178.88410e-04 0.117 9.0172597469e+03 4.2843375753e+01
8189.43910e-04 0.117 7.0496495917e+03 3.2771032724e+01
8199.99410e-04 0.117 5.2030483682e+03 2.4113557144e+01
8201.05491e-03 0.117 3.5988976711e+03 1.7160773658e+01
8211.11041e-03 0.117 2.2996060652e+03 1.2016626459e+01
8221.22141e-03 0.117 6.4766590598e+02 6.0373017740e+00
8231.33241e-03 0.117 4.1963483264e+01 4.5215452974e+00
8241.44341e-03 0.117 6.3370708246e+01 5.1054681903e+00
8251.55441e-03 0.117 3.0736750577e+02 5.9176165298e+00
8261.66541e-03 0.117 5.0327682399e+02 5.9815000189e+00
8271.77641e-03 0.117 5.4084331454e+02 5.1634639625e+00
8281.88741e-03 0.117 4.3488671756e+02 3.8535158148e+00
8291.99841e-03 0.117 2.6322287860e+02 2.5824997753e+00
8302.10941e-03 0.117 1.0793633150e+02 1.7315517194e+00
8312.22041e-03 0.117 1.8474448850e+01 1.4077213604e+00
8322.33141e-03 0.117 1.5864062279e+00 1.4771560682e+00
8332.44241e-03 0.117 3.2267213848e+01 1.6916253448e+00
8342.55341e-03 0.117 7.4289116207e+01 1.8274751193e+00
8352.66441e-03 0.117 9.9000521929e+01 1.7706812289e+00
836"""
837
838def main():
839    """
840    Run tests given is sys.argv.
841
842    Returns 0 if success or 1 if any tests fail.
843    """
844    import sys
845    import xmlrunner
846
847    suite = unittest.TestSuite()
848    suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]))
849
850    runner = xmlrunner.XMLTestRunner(output='logs')
851    result = runner.run(suite)
852    return 1 if result.failures or result.errors else 0
853
854
855############################################################################
856# usage demo
857############################################################################
858
859def _eval_demo_1d(resolution, title):
860    from sasmodels import core
861    from sasmodels.models import cylinder
862    ## or alternatively:
863    # cylinder = core.load_model_definition('cylinder')
864    model = core.load_model(cylinder)
865
866    kernel = core.make_kernel(model, [resolution.q_calc])
867    theory = core.call_kernel(kernel, {'length':210, 'radius':500})
868    Iq = resolution.apply(theory)
869
870    import matplotlib.pyplot as plt
871    plt.loglog(resolution.q_calc, theory, label='unsmeared')
872    plt.loglog(resolution.q, Iq, label='smeared', hold=True)
873    plt.legend()
874    plt.title(title)
875    plt.xlabel("Q (1/Ang)")
876    plt.ylabel("I(Q) (1/cm)")
877
878def demo_pinhole_1d():
879    q = np.logspace(-3, -1, 400)
880    q_width = 0.1*q
881    resolution = Pinhole1D(q, q_width)
882    _eval_demo_1d(resolution, title="10% dQ/Q Pinhole Resolution")
883
884def demo_slit_1d():
885    q = np.logspace(-3, -1, 400)
886    qx_width = 0.005
887    qy_width = 0.0
888    resolution = Slit1D(q, qx_width, qy_width)
889    _eval_demo_1d(resolution, title="0.005 Qx Slit Resolution")
890
891def demo():
892    import matplotlib.pyplot as plt
893    plt.subplot(121)
894    demo_pinhole_1d()
895    plt.subplot(122)
896    demo_slit_1d()
897    plt.show()
898
899
900if __name__ == "__main__":
901    #demo()
902    main()
Note: See TracBrowser for help on using the repository browser.