source: sasmodels/sasmodels/resolution.py @ 6871c9e

core_shell_microgelscostrafo411magnetic_modelrelease_v0.94release_v0.95ticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 6871c9e was 6871c9e, checked in by Paul Kienzle <pkienzle@…>, 9 years ago

add slit-smeared ellipsoid test

  • Property mode set to 100644
File size: 30.8 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):
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) \
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 geometric_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 steps the same size as the first
270    interval.  Extrapolation above uses steps 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        delta = q[1] - q[0]
279        q_low = np.arange(q_min, q[0], delta)
280    else:
281        q_low = []
282    if q_max > q[-1]:
283        delta = q[-1] - q[-2]
284        q_high = np.arange(q[-1]+delta, q_max, delta)
285    else:
286        q_high = []
287    return np.concatenate([q_low, q, q_high])
288
289
290def geometric_extrapolation(q, q_min, q_max):
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    Starting at $q_1$ and stepping geometrically by $\Delta q$
298    to $q_n$ in $n$ points gives a geometric average of:
299
300    .. math::
301
302         \log \Delta q = (\log q_n - log q_1) / (n - 1)
303
304    From this we can compute the number of steps required to extend $q$
305    from $q_n$ to $q_\text{max}$ by $\Delta q$ as:
306
307    .. math::
308
309         n_\text{extend} = (\log q_\text{max} - \log q_n) / \log \Delta q
310
311    Substituting:
312
313        n_\text{extend} = (n-1) (\log q_\text{max} - \log q_n)
314            / (\log q_n - log q_1)
315    """
316    q = np.sort(q)
317    delta_q = (len(q)-1)/(log(q[-1]) - log(q[0]))
318    if q_min < q[0]:
319        if q_min < 0: q_min = q[0]/10
320        n_low = delta_q * (log(q[0])-log(q_min))
321        q_low  = np.logspace(log10(q_min), log10(q[0]), np.ceil(n_low)+1)[:-1]
322    else:
323        q_low = []
324    if q_max > q[-1]:
325        n_high = delta_q * (log(q_max)-log(q[-1]))
326        q_high = np.logspace(log10(q[-1]), log10(q_max), np.ceil(n_high)+1)[1:]
327    else:
328        q_high = []
329    return np.concatenate([q_low, q, q_high])
330
331
332############################################################################
333# unit tests
334############################################################################
335import unittest
336
337
338def eval_form(q, form, pars):
339    from sasmodels import core
340    kernel = core.make_kernel(form, [q])
341    theory = core.call_kernel(kernel, pars)
342    kernel.release()
343    return theory
344
345
346def gaussian(q, q0, dq):
347    from numpy import exp, pi
348    return exp(-0.5*((q-q0)/dq)**2)/(sqrt(2*pi)*dq)
349
350
351def romberg_slit_1d(q, delta_qv, form, pars):
352    """
353    Romberg integration for slit resolution.
354
355    This is an adaptive integration technique.  It is called with settings
356    that make it slow to evaluate but give it good accuracy.
357    """
358    from scipy.integrate import romberg
359
360    if any(k not in form.info['defaults'] for k in pars.keys()):
361        keys = set(form.info['defaults'].keys())
362        extra = set(pars.keys()) - keys
363        raise ValueError("bad parameters: [%s] not in [%s]"%
364                         (", ".join(sorted(extra)), ", ".join(sorted(keys))))
365
366    _fn = lambda u, q0: eval_form(sqrt(q0**2 + u**2), form, pars)
367    r = [romberg(_fn, 0, delta_qv[0], args=(qi,),
368                 divmax=100, vec_func=True, tol=0, rtol=1e-8)
369         for qi in q]
370    # r should be [float, ...], but it is [array([float]), array([float]),...]
371    return np.asarray(r).flatten()/delta_qv[0]
372
373
374def romberg_pinhole_1d(q, q_width, form, pars):
375    """
376    Romberg integration for pinhole resolution.
377
378    This is an adaptive integration technique.  It is called with settings
379    that make it slow to evaluate but give it good accuracy.
380    """
381    from scipy.integrate import romberg
382
383    if any(k not in form.info['defaults'] for k in pars.keys()):
384        keys = set(form.info['defaults'].keys())
385        extra = set(pars.keys()) - keys
386        raise ValueError("bad parameters: [%s] not in [%s]"%
387                         (", ".join(sorted(extra)), ", ".join(sorted(keys))))
388
389    _fn = lambda q, q0, dq: eval_form(q, form, pars)*gaussian(q, q0, dq)
390    r = [romberg(_fn, max(qi-5*dqi,0.01*q[0]), qi+5*dqi, args=(qi, dqi),
391                 divmax=100, vec_func=True, tol=0, rtol=1e-8)
392         for qi,dqi in zip(q,q_width)]
393    return np.asarray(r).flatten()
394
395
396class ResolutionTest(unittest.TestCase):
397
398    def setUp(self):
399        self.x = 0.001*np.arange(1, 11)
400        self.y = self.Iq(self.x)
401
402    def Iq(self, q):
403        "Linear function for resolution unit test"
404        return 12.0 - 1000.0*q
405
406    def test_perfect(self):
407        """
408        Perfect resolution and no smearing.
409        """
410        resolution = Perfect1D(self.x)
411        theory = self.Iq(resolution.q_calc)
412        output = resolution.apply(theory)
413        np.testing.assert_equal(output, self.y)
414
415    def test_slit_zero(self):
416        """
417        Slit smearing with perfect resolution.
418        """
419        resolution = Slit1D(self.x, width=0, height=0, q_calc=self.x)
420        theory = self.Iq(resolution.q_calc)
421        output = resolution.apply(theory)
422        np.testing.assert_equal(output, self.y)
423
424    @unittest.skip("not yet supported")
425    def test_slit_high(self):
426        """
427        Slit smearing with height 0.005
428        """
429        resolution = Slit1D(self.x, width=0, height=0.005, q_calc=self.x)
430        theory = self.Iq(resolution.q_calc)
431        output = resolution.apply(theory)
432        answer = [ 9.0618, 8.6402, 8.1187, 7.1392, 6.1528,
433                   5.5555, 4.5584, 3.5606, 2.5623, 2.0000 ]
434        np.testing.assert_allclose(output, answer, atol=1e-4)
435
436    @unittest.skip("not yet supported")
437    def test_slit_both_high(self):
438        """
439        Slit smearing with width < 100*height.
440        """
441        q = np.logspace(-4, -1, 10)
442        resolution = Slit1D(q, width=0.2, height=np.inf)
443        theory = 1000*self.Iq(resolution.q_calc**4)
444        output = resolution.apply(theory)
445        answer = [ 8.85785, 8.43012, 7.92687, 6.94566, 6.03660,
446                   5.40363, 4.40655, 3.40880, 2.41058, 2.00000 ]
447        np.testing.assert_allclose(output, answer, atol=1e-4)
448
449    @unittest.skip("not yet supported")
450    def test_slit_wide(self):
451        """
452        Slit smearing with width 0.0002
453        """
454        resolution = Slit1D(self.x, width=0.0002, height=0, q_calc=self.x)
455        theory = self.Iq(resolution.q_calc)
456        output = resolution.apply(theory)
457        answer = [ 11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0 ]
458        np.testing.assert_allclose(output, answer, atol=1e-4)
459
460    @unittest.skip("not yet supported")
461    def test_slit_both_wide(self):
462        """
463        Slit smearing with width > 100*height.
464        """
465        resolution = Slit1D(self.x, width=0.0002, height=0.000001,
466                            q_calc=self.x)
467        theory = self.Iq(resolution.q_calc)
468        output = resolution.apply(theory)
469        answer = [ 11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0 ]
470        np.testing.assert_allclose(output, answer, atol=1e-4)
471
472    def test_pinhole_zero(self):
473        """
474        Pinhole smearing with perfect resolution
475        """
476        resolution = Pinhole1D(self.x, 0.0*self.x)
477        theory = self.Iq(resolution.q_calc)
478        output = resolution.apply(theory)
479        np.testing.assert_equal(output, self.y)
480
481    def test_pinhole(self):
482        """
483        Pinhole smearing with dQ = 0.001 [Note: not dQ/Q = 0.001]
484        """
485        resolution = Pinhole1D(self.x, 0.001*np.ones_like(self.x),
486                               q_calc=self.x)
487        theory = 12.0-1000.0*resolution.q_calc
488        output = resolution.apply(theory)
489        answer = [ 10.44785079, 9.84991299, 8.98101708,
490                  7.99906585, 6.99998311, 6.00001689,
491                  5.00093415, 4.01898292, 3.15008701, 2.55214921]
492        np.testing.assert_allclose(output, answer, atol=1e-8)
493
494
495class IgorComparisonTest(unittest.TestCase):
496
497    def setUp(self):
498        self.pars = TEST_PARS_PINHOLE_SPHERE
499        from sasmodels import core
500        from sasmodels.models import sphere
501        self.model = core.load_model(sphere, dtype='double')
502
503    def Iq_sphere(self, pars, resolution):
504        from sasmodels import core
505        kernel = core.make_kernel(self.model, [resolution.q_calc])
506        theory = core.call_kernel(kernel, pars)
507        result = resolution.apply(theory)
508        kernel.release()
509        return result
510
511    def compare(self, q, output, answer, tolerance):
512        err = (output - answer)/answer
513        idx = abs(err) >= tolerance
514        problem = zip(q[idx], output[idx], answer[idx], err[idx])
515        print "\n".join(str(v) for v in problem)
516        np.testing.assert_allclose(output, answer, rtol=tolerance)
517
518    def test_perfect(self):
519        """
520        Compare sphere model with NIST Igor SANS, no resolution smearing.
521        """
522        pars = TEST_PARS_SLIT_SPHERE
523        data_string = TEST_DATA_SLIT_SPHERE
524
525        data = np.loadtxt(data_string.split('\n')).T
526        q, width, answer, _ = data
527        resolution = Perfect1D(q)
528        output = self.Iq_sphere(pars, resolution)
529        self.compare(q, output, answer, 1e-6)
530
531    def test_pinhole(self):
532        """
533        Compare pinhole resolution smearing with NIST Igor SANS
534        """
535        pars = TEST_PARS_PINHOLE_SPHERE
536        data_string = TEST_DATA_PINHOLE_SPHERE
537
538        data = np.loadtxt(data_string.split('\n')).T
539        q, q_width, answer = data
540        resolution = Pinhole1D(q, q_width)
541        output = self.Iq_sphere(pars, resolution)
542        # TODO: relative error should be lower
543        self.compare(q, output, answer, 3e-4)
544
545    def test_pinhole_romberg(self):
546        """
547        Compare pinhole resolution smearing with romberg integration result.
548        """
549        pars = TEST_PARS_PINHOLE_SPHERE
550        data_string = TEST_DATA_PINHOLE_SPHERE
551        pars['radius'] *= 5
552        radius = pars['radius']
553
554        data = np.loadtxt(data_string.split('\n')).T
555        q, q_width, answer = data
556        answer = romberg_pinhole_1d(q, q_width, self.model, pars)
557        ## Getting 0.1% requires 5 sigma and 200 points per fringe
558        #q_calc = interpolate(pinhole_extend_q(q, q_width, nsigma=5),
559        #                     2*np.pi/radius/200)
560        #tol = 0.001
561        ## The default 3 sigma and no extra points gets 1%
562        q_calc, tol = None, 0.01
563        resolution = Pinhole1D(q, q_width, q_calc=q_calc)
564        output = self.Iq_sphere(pars, resolution)
565        if 0: # debug plot
566            import matplotlib.pyplot as plt
567            resolution = Perfect1D(q)
568            source = self.Iq_sphere(pars, resolution)
569            plt.loglog(q, source, '.')
570            plt.loglog(q, answer, '-', hold=True)
571            plt.loglog(q, output, '-', hold=True)
572            plt.show()
573        self.compare(q, output, answer, tol)
574
575    def test_slit(self):
576        """
577        Compare slit resolution smearing with NIST Igor SANS
578        """
579        pars = TEST_PARS_SLIT_SPHERE
580        data_string = TEST_DATA_SLIT_SPHERE
581
582        data = np.loadtxt(data_string.split('\n')).T
583        q, delta_qv, _, answer = data
584        resolution = Slit1D(q, width=delta_qv, height=0)
585        output = self.Iq_sphere(pars, resolution)
586        # TODO: eliminate Igor test since it is too inaccurate to be useful.
587        # This means we can eliminate the test data as well, and instead
588        # use a generated q vector.
589        self.compare(q, output, answer, 0.5)
590
591    def test_slit_romberg(self):
592        """
593        Compare slit resolution smearing with romberg integration result.
594        """
595        pars = TEST_PARS_SLIT_SPHERE
596        data_string = TEST_DATA_SLIT_SPHERE
597        radius = pars['radius']
598
599        data = np.loadtxt(data_string.split('\n')).T
600        q, delta_qv, _, answer = data
601        answer = romberg_slit_1d(q, delta_qv, self.model, pars)
602        q_calc = slit_extend_q(interpolate(q, 2*np.pi/radius/20),
603                               delta_qv[0], delta_qv[0])
604        resolution = Slit1D(q, width=delta_qv, height=0, q_calc=q_calc)
605        output = self.Iq_sphere(pars, resolution)
606        # TODO: relative error should be lower
607        self.compare(q, output, answer, 0.025)
608
609    def test_ellipsoid(self):
610        """
611        Compare romberg integration for ellipsoid model.
612        """
613        from .core import load_model
614        pars = {
615            'scale':0.05,
616            'rpolar':500, 'requatorial':15000,
617            'sld':6, 'solvent_sld': 1,
618            }
619        form = load_model('ellipsoid', dtype='double')
620        q = np.logspace(log10(4e-5),log10(2.5e-2), 68)
621        delta_qv = [0.117]
622        resolution = Slit1D(q, width=delta_qv, height=0)
623        answer = romberg_slit_1d(q, delta_qv, form, pars)
624        output = resolution.apply(eval_form(resolution.q_calc, form, pars))
625        # TODO: 10% is too much error; use better algorithm
626        self.compare(q, output, answer, 0.1)
627
628    #TODO: can sas q spacing be too sparse for the resolution calculation?
629    @unittest.skip("suppress sparse data test; not supported by current code")
630    def test_pinhole_sparse(self):
631        """
632        Compare pinhole resolution smearing with NIST Igor SANS on sparse data
633        """
634        pars = TEST_PARS_PINHOLE_SPHERE
635        data_string = TEST_DATA_PINHOLE_SPHERE
636
637        data = np.loadtxt(data_string.split('\n')).T
638        q, q_width, answer = data[:, ::20] # Take every nth point
639        resolution = Pinhole1D(q, q_width)
640        output = self.Iq_sphere(pars, resolution)
641        self.compare(q, output, answer, 1e-6)
642
643
644
645# pinhole sphere parameters
646TEST_PARS_PINHOLE_SPHERE = {
647    'scale': 1.0, 'background': 0.01,
648    'radius': 60.0, 'sld': 1, 'solvent_sld': 6.3,
649    }
650# Q, dQ, I(Q) calculated by NIST Igor SANS package
651TEST_DATA_PINHOLE_SPHERE = """\
6520.001278 0.0002847 2538.41176383
6530.001562 0.0002905 2536.91820405
6540.001846 0.0002956 2535.13182479
6550.002130 0.0003017 2533.06217813
6560.002414 0.0003087 2530.70378586
6570.002698 0.0003165 2528.05024192
6580.002982 0.0003249 2525.10408349
6590.003266 0.0003340 2521.86667499
6600.003550 0.0003437 2518.33907750
6610.003834 0.0003539 2514.52246995
6620.004118 0.0003646 2510.41798319
6630.004402 0.0003757 2506.02690988
6640.004686 0.0003872 2501.35067884
6650.004970 0.0003990 2496.38678318
6660.005253 0.0004112 2491.16237596
6670.005537 0.0004237 2485.63911673
6680.005821 0.0004365 2479.83657083
6690.006105 0.0004495 2473.75676948
6700.006389 0.0004628 2467.40145990
6710.006673 0.0004762 2460.77293372
6720.006957 0.0004899 2453.86724627
6730.007241 0.0005037 2446.69623838
6740.007525 0.0005177 2439.25775219
6750.007809 0.0005318 2431.55421398
6760.008093 0.0005461 2423.58785521
6770.008377 0.0005605 2415.36158137
6780.008661 0.0005750 2406.87009473
6790.008945 0.0005896 2398.12841186
6800.009229 0.0006044 2389.13360806
6810.009513 0.0006192 2379.88958042
6820.009797 0.0006341 2370.39776774
6830.010080 0.0006491 2360.69528793
6840.010360 0.0006641 2350.85169027
6850.010650 0.0006793 2340.42023633
6860.010930 0.0006945 2330.11206013
6870.011220 0.0007097 2319.20109972
6880.011500 0.0007251 2308.43503981
6890.011780 0.0007404 2297.44820179
6900.012070 0.0007558 2285.83853677
6910.012350 0.0007713 2274.41290746
6920.012640 0.0007868 2262.36219581
6930.012920 0.0008024 2250.51169731
6940.013200 0.0008180 2238.45596231
6950.013490 0.0008336 2225.76495666
6960.013770 0.0008493 2213.29618391
6970.014060 0.0008650 2200.19110751
6980.014340 0.0008807 2187.34050325
6990.014620 0.0008965 2174.30529864
7000.014910 0.0009123 2160.61632548
7010.015190 0.0009281 2147.21038112
7020.015470 0.0009440 2133.62023580
7030.015760 0.0009598 2119.37907426
7040.016040 0.0009757 2105.45234903
7050.016330 0.0009916 2090.86319102
7060.016610 0.0010080 2076.60576032
7070.016890 0.0010240 2062.19214565
7080.017180 0.0010390 2047.10550219
7090.017460 0.0010550 2032.38715621
7100.017740 0.0010710 2017.52560123
7110.018030 0.0010880 2001.99124318
7120.018310 0.0011040 1986.84662060
7130.018600 0.0011200 1971.03389745
7140.018880 0.0011360 1955.61395119
7150.019160 0.0011520 1940.08291563
7160.019450 0.0011680 1923.87672225
7170.019730 0.0011840 1908.10656374
7180.020020 0.0012000 1891.66297192
7190.020300 0.0012160 1875.66789021
7200.020580 0.0012320 1859.56357196
7210.020870 0.0012490 1842.79468290
7220.021150 0.0012650 1826.50064489
7230.021430 0.0012810 1810.11533702
7240.021720 0.0012970 1793.06840882
7250.022000 0.0013130 1776.51153580
7260.022280 0.0013290 1759.87201249
7270.022570 0.0013460 1742.57354412
7280.022850 0.0013620 1725.79397319
7290.023140 0.0013780 1708.35831550
7300.023420 0.0013940 1691.45256069
7310.023700 0.0014110 1674.48561783
7320.023990 0.0014270 1656.86525366
7330.024270 0.0014430 1639.79847285
7340.024550 0.0014590 1622.68887088
7350.024840 0.0014760 1604.96421100
7360.025120 0.0014920 1587.85768129
7370.025410 0.0015080 1569.99297335
7380.025690 0.0015240 1552.84580279
7390.025970 0.0015410 1535.54074115
7400.026260 0.0015570 1517.75249337
7410.026540 0.0015730 1500.40115023
7420.026820 0.0015900 1483.03632237
7430.027110 0.0016060 1465.05942429
7440.027390 0.0016220 1447.67682181
7450.027670 0.0016390 1430.46495191
7460.027960 0.0016550 1412.49232282
7470.028240 0.0016710 1395.13182318
7480.028520 0.0016880 1377.93439837
7490.028810 0.0017040 1359.99528971
7500.029090 0.0017200 1342.67274512
7510.029370 0.0017370 1325.55375609
752"""
753
754# Slit sphere parameters
755TEST_PARS_SLIT_SPHERE = {
756    'scale': 0.01, 'background': 0.01,
757    'radius': 60000, 'sld': 1, 'solvent_sld': 4,
758    }
759# Q dQ I(Q) I_smeared(Q)
760TEST_DATA_SLIT_SPHERE = """\
7612.26097e-05 0.117 5.5781372896e+09 1.4626077708e+06
7622.53847e-05 0.117 5.0363141458e+09 1.3117318023e+06
7632.81597e-05 0.117 4.4850108103e+09 1.1594863713e+06
7643.09347e-05 0.117 3.9364658459e+09 1.0094881630e+06
7653.37097e-05 0.117 3.4019975074e+09 8.6518941303e+05
7663.92597e-05 0.117 2.4139519814e+09 6.0232158311e+05
7674.48097e-05 0.117 1.5816877820e+09 3.8739994090e+05
7685.03597e-05 0.117 9.3715407224e+08 2.2745304775e+05
7695.59097e-05 0.117 4.8387917428e+08 1.2101295768e+05
7706.14597e-05 0.117 2.0193586928e+08 6.0055107771e+04
7716.70097e-05 0.117 5.5886110911e+07 3.2749521065e+04
7727.25597e-05 0.117 3.7782348010e+06 2.6350963616e+04
7737.81097e-05 0.117 5.3407817904e+06 2.9624963314e+04
7748.36597e-05 0.117 2.7975485523e+07 3.4403962254e+04
7758.92097e-05 0.117 4.9845448282e+07 3.6130017903e+04
7769.47597e-05 0.117 6.0092588905e+07 3.3495107849e+04
7771.00310e-04 0.117 5.6823430831e+07 2.7475726279e+04
7781.05860e-04 0.117 4.3857024036e+07 2.0144282226e+04
7791.11410e-04 0.117 2.7277144760e+07 1.3647403260e+04
7801.22510e-04 0.117 3.3119334113e+06 6.6519711526e+03
7811.33610e-04 0.117 1.4412859402e+06 6.9726212813e+03
7821.44710e-04 0.117 8.5620162463e+06 8.1441335775e+03
7831.55810e-04 0.117 9.6957429033e+06 6.4559996521e+03
7841.66910e-04 0.117 4.3818341914e+06 3.6252154156e+03
7851.78010e-04 0.117 2.7448997387e+05 2.4006505342e+03
7861.89110e-04 0.117 8.0472009936e+05 2.8187789089e+03
7872.00210e-04 0.117 2.8149552834e+06 3.0915662855e+03
7882.11310e-04 0.117 2.7510907861e+06 2.3722530293e+03
7892.22410e-04 0.117 1.0053133293e+06 1.4473468311e+03
7902.33510e-04 0.117 5.8428305052e+03 1.2048540556e+03
7912.44610e-04 0.117 5.1699305004e+05 1.4625670042e+03
7922.55710e-04 0.117 1.2120227268e+06 1.5010705973e+03
7932.66810e-04 0.117 9.7896842846e+05 1.1336343426e+03
7942.77910e-04 0.117 2.5507264791e+05 8.1848818080e+02
7953.05660e-04 0.117 5.2403101181e+05 7.4913374357e+02
7963.33410e-04 0.117 5.8699343809e+04 4.4669964560e+02
7973.61160e-04 0.117 3.0844327150e+05 4.6774007542e+02
7983.88910e-04 0.117 8.3360142970e+03 2.7169550220e+02
7994.16660e-04 0.117 1.8630080583e+05 3.0710983679e+02
8004.44410e-04 0.117 3.1616804732e-01 1.7959006831e+02
8014.72160e-04 0.117 1.1299016314e+05 2.0763952339e+02
8024.99910e-04 0.117 2.9952522747e+03 1.2536542765e+02
8035.27660e-04 0.117 6.7625695649e+04 1.4013969777e+02
8045.55410e-04 0.117 7.6927460089e+03 8.2145593180e+01
8056.10910e-04 0.117 1.1229057779e+04 8.4519745643e+01
8066.66410e-04 0.117 1.3035567943e+04 8.1554625609e+01
8077.21910e-04 0.117 1.3309931343e+04 7.4437319172e+01
8087.77410e-04 0.117 1.2462626212e+04 6.4697088261e+01
8098.32910e-04 0.117 1.0912927143e+04 5.3773301044e+01
8108.88410e-04 0.117 9.0172597469e+03 4.2843375753e+01
8119.43910e-04 0.117 7.0496495917e+03 3.2771032724e+01
8129.99410e-04 0.117 5.2030483682e+03 2.4113557144e+01
8131.05491e-03 0.117 3.5988976711e+03 1.7160773658e+01
8141.11041e-03 0.117 2.2996060652e+03 1.2016626459e+01
8151.22141e-03 0.117 6.4766590598e+02 6.0373017740e+00
8161.33241e-03 0.117 4.1963483264e+01 4.5215452974e+00
8171.44341e-03 0.117 6.3370708246e+01 5.1054681903e+00
8181.55441e-03 0.117 3.0736750577e+02 5.9176165298e+00
8191.66541e-03 0.117 5.0327682399e+02 5.9815000189e+00
8201.77641e-03 0.117 5.4084331454e+02 5.1634639625e+00
8211.88741e-03 0.117 4.3488671756e+02 3.8535158148e+00
8221.99841e-03 0.117 2.6322287860e+02 2.5824997753e+00
8232.10941e-03 0.117 1.0793633150e+02 1.7315517194e+00
8242.22041e-03 0.117 1.8474448850e+01 1.4077213604e+00
8252.33141e-03 0.117 1.5864062279e+00 1.4771560682e+00
8262.44241e-03 0.117 3.2267213848e+01 1.6916253448e+00
8272.55341e-03 0.117 7.4289116207e+01 1.8274751193e+00
8282.66441e-03 0.117 9.9000521929e+01 1.7706812289e+00
829"""
830
831def main():
832    """
833    Run tests given is sys.argv.
834
835    Returns 0 if success or 1 if any tests fail.
836    """
837    import sys
838    import xmlrunner
839
840    suite = unittest.TestSuite()
841    suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]))
842
843    runner = xmlrunner.XMLTestRunner(output='logs')
844    result = runner.run(suite)
845    return 1 if result.failures or result.errors else 0
846
847
848############################################################################
849# usage demo
850############################################################################
851
852def _eval_demo_1d(resolution, title):
853    from sasmodels import core
854    from sasmodels.models import cylinder
855    ## or alternatively:
856    # cylinder = core.load_model_definition('cylinder')
857    model = core.load_model(cylinder)
858
859    kernel = core.make_kernel(model, [resolution.q_calc])
860    theory = core.call_kernel(kernel, {'length':210, 'radius':500})
861    Iq = resolution.apply(theory)
862
863    import matplotlib.pyplot as plt
864    plt.loglog(resolution.q_calc, theory, label='unsmeared')
865    plt.loglog(resolution.q, Iq, label='smeared', hold=True)
866    plt.legend()
867    plt.title(title)
868    plt.xlabel("Q (1/Ang)")
869    plt.ylabel("I(Q) (1/cm)")
870
871def demo_pinhole_1d():
872    q = np.logspace(-3, -1, 400)
873    q_width = 0.1*q
874    resolution = Pinhole1D(q, q_width)
875    _eval_demo_1d(resolution, title="10% dQ/Q Pinhole Resolution")
876
877def demo_slit_1d():
878    q = np.logspace(-3, -1, 400)
879    qx_width = 0.005
880    qy_width = 0.0
881    resolution = Slit1D(q, qx_width, qy_width)
882    _eval_demo_1d(resolution, title="0.005 Qx Slit Resolution")
883
884def demo():
885    import matplotlib.pyplot as plt
886    plt.subplot(121)
887    demo_pinhole_1d()
888    plt.subplot(122)
889    demo_slit_1d()
890    plt.show()
891
892
893if __name__ == "__main__":
894    #demo()
895    main()
Note: See TracBrowser for help on using the repository browser.