source: sasmodels/sasmodels/resolution.py @ dbde9f8

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

fix slit smearing width only slits

  • Property mode set to 100644
File size: 32.9 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 Resolution(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(Resolution):
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(Resolution):
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(Resolution):
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=0., q_calc=None):
95        # Remember what width/height was used even though we won't need them
96        # after the weight matrix is constructed
97        self.width, self.height = width, height
98
99        # Allow independent resolution on each point even though it is not
100        # needed in practice.
101        if np.isscalar(width):
102            width = np.ones(len(q))*width
103        else:
104            width = np.asarray(width)
105        if np.isscalar(height):
106            height = np.ones(len(q))*height
107        else:
108            height = np.asarray(height)
109
110        self.q = q.flatten()
111        self.q_calc = slit_extend_q(q, width, height) \
112            if q_calc is None else np.sort(q_calc)
113        self.weight_matrix = \
114            slit_resolution(self.q_calc, self.q, width, height)
115
116    def apply(self, theory):
117        return apply_resolution_matrix(self.weight_matrix, theory)
118
119
120def apply_resolution_matrix(weight_matrix, theory):
121    """
122    Apply the resolution weight matrix to the computed theory function.
123    """
124    #print "apply shapes", theory.shape, weight_matrix.shape
125    Iq = np.dot(theory[None,:], weight_matrix)
126    #print "result shape",Iq.shape
127    return Iq.flatten()
128
129
130def pinhole_resolution(q_calc, q, q_width):
131    """
132    Compute the convolution matrix *W* for pinhole resolution 1-D data.
133
134    Each row *W[i]* determines the normalized weight that the corresponding
135    points *q_calc* contribute to the resolution smeared point *q[i]*.  Given
136    *W*, the resolution smearing can be computed using *dot(W,q)*.
137
138    *q_calc* must be increasing.  *q_width* must be greater than zero.
139    """
140    # The current algorithm is a midpoint rectangle rule.  In the test case,
141    # neither trapezoid nor Simpson's rule improved the accuracy.
142    edges = bin_edges(q_calc)
143    edges[edges<0.0] = 0.0 # clip edges below zero
144    G = erf( (edges[:,None] - q[None,:]) / (sqrt(2.0)*q_width)[None,:] )
145    weights = G[1:] - G[:-1]
146    weights /= np.sum(weights, axis=0)[None,:]
147    return weights
148
149
150def slit_resolution(q_calc, q, width, height):
151    r"""
152    Build a weight matrix to compute *I_s(q)* from *I(q_calc)*, given
153    $q_v$ = *width* and $q_h$ = *height*.
154
155    *width* and *height* are scalars since current instruments use the
156    same slit settings for all measurement points.
157
158    If slit height is large relative to width, use:
159
160    .. math::
161
162        I_s(q_o) = \frac{1}{\Delta q_v}
163            \int_0^{\Delta q_v} I(\sqrt{q_o^2 + u^2} du
164
165    If slit width is large relative to height, use:
166
167    .. math::
168
169        I_s(q_o) = \frac{1}{2 \Delta q_v}
170            \int_{-\Delta q_v}^{\Delta q_v} I(u) du
171    """
172    #np.set_printoptions(precision=6, linewidth=10000)
173
174    # The current algorithm is a midpoint rectangle rule.
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    weights = np.zeros((len(q), len(q_calc)), 'd')
178
179    for i, (qi, w, h) in enumerate(zip(q, width, height)):
180        if w == 0. and h == 0.:
181            # Perfect resolution, so return the theory value directly.
182            # Note: assumes that q is a subset of q_calc.  If qi need not be
183            # in q_calc, then we can do a weighted interpolation by looking
184            # up qi in q_calc, then weighting the result by the relative
185            # distance to the neighbouring points.
186            weights[i, :] = (q_calc == qi)
187        elif h == 0 or w > 100.0 * h:
188            # Convert bin edges from q to u
189            u_limit = np.sqrt(qi**2 + w**2)
190            u_edges = q_edges**2 - qi**2
191            u_edges[q_edges < qi] = 0.
192            u_edges[q_edges > u_limit] = u_limit**2 - qi**2
193            weights[i, :] = np.diff(np.sqrt(u_edges))/w
194            #print "i, qi",i,qi,qi+width
195            #print q_calc
196            #print weights[i,:]
197        elif w == 0:
198            in_x = (q_calc >= qi-h) * (q_calc <= qi+h)
199            weights[i,:] = in_x * np.diff(q_edges) / (2*h)
200
201    return weights.T
202
203
204def pinhole_extend_q(q, q_width, nsigma=3):
205    """
206    Given *q* and *q_width*, find a set of sampling points *q_calc* so
207    that each point I(q) has sufficient support from the underlying
208    function.
209    """
210    q_min, q_max = np.min(q - nsigma*q_width), np.max(q + nsigma*q_width)
211    return linear_extrapolation(q, q_min, q_max)
212
213
214def slit_extend_q(q, width, height):
215    """
216    Given *q*, *width* and *height*, find a set of sampling points *q_calc* so
217    that each point I(q) has sufficient support from the underlying
218    function.
219    """
220    q_min, q_max = np.min(q), np.max(np.sqrt(q**2 + width**2))
221    return geometric_extrapolation(q, q_min, q_max)
222
223
224def bin_edges(x):
225    """
226    Determine bin edges from bin centers, assuming that edges are centered
227    between the bins.
228
229    Note: this uses the arithmetic mean, which may not be appropriate for
230    log-scaled data.
231    """
232    if len(x) < 2 or (np.diff(x)<0).any():
233        raise ValueError("Expected bins to be an increasing set")
234    edges = np.hstack([
235        x[0]  - 0.5*(x[1]  - x[0]),  # first point minus half first interval
236        0.5*(x[1:] + x[:-1]),        # mid points of all central intervals
237        x[-1] + 0.5*(x[-1] - x[-2]), # last point plus half last interval
238        ])
239    return edges
240
241
242def interpolate(q, max_step):
243    """
244    Returns *q_calc* with points spaced at most max_step apart.
245    """
246    step = np.diff(q)
247    index = step>max_step
248    if np.any(index):
249        inserts = []
250        for q_i,step_i in zip(q[:-1][index],step[index]):
251            n = np.ceil(step_i/max_step)
252            inserts.extend(q_i + np.arange(1,n)*(step_i/n))
253        # Extend a couple of fringes beyond the end of the data
254        inserts.extend(q[-1] + np.arange(1,8)*max_step)
255        q_calc = np.sort(np.hstack((q,inserts)))
256    else:
257        q_calc = q
258    return q_calc
259
260
261def linear_extrapolation(q, q_min, q_max):
262    """
263    Extrapolate *q* out to [*q_min*, *q_max*] using the step size in *q* as
264    a guide.  Extrapolation below uses about the same size as the first
265    interval.  Extrapolation above uses about the same size as the final
266    interval.
267
268    if *q_min* is zero or less then *q[0]/10* is used instead.
269    """
270    q = np.sort(q)
271    if q_min < q[0]:
272        if q_min <= 0: q_min = q[0]/10
273        n_low = np.ceil((q[0]-q_min) / (q[1]-q[0])) if q[1]>q[0] else 15
274        q_low = np.linspace(q_min, q[0], n_low+1)[:-1]
275    else:
276        q_low = []
277    if q_max > q[-1]:
278        n_high = np.ceil((q_max-q[-1]) / (q[-1]-q[-2])) if q[-1]>q[-2] else 15
279        q_high = np.linspace(q[-1], q_max, n_high+1)[1:]
280    else:
281        q_high = []
282    return np.concatenate([q_low, q, q_high])
283
284
285def geometric_extrapolation(q, q_min, q_max, points_per_decade=None):
286    r"""
287    Extrapolate *q* to [*q_min*, *q_max*] using geometric steps, with the
288    average geometric step size in *q* as the step size.
289
290    if *q_min* is zero or less then *q[0]/10* is used instead.
291
292    *points_per_decade* sets the ratio between consecutive steps such
293    that there will be $n$ points used for every factor of 10 increase
294    in *q*.
295
296    If *points_per_decade* is not given, it will be estimated as follows.
297    Starting at $q_1$ and stepping geometrically by $\Delta q$ to $q_n$
298    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    if points_per_decade is None:
318        log_delta_q = (len(q) - 1) / (log(q[-1]) - log(q[0]))
319    else:
320        log_delta_q = log(10.) / points_per_decade
321    if q_min < q[0]:
322        if q_min < 0: q_min = q[0]/10
323        n_low = log_delta_q * (log(q[0])-log(q_min))
324        q_low  = np.logspace(log10(q_min), log10(q[0]), np.ceil(n_low)+1)[:-1]
325    else:
326        q_low = []
327    if q_max > q[-1]:
328        n_high = log_delta_q * (log(q_max)-log(q[-1]))
329        q_high = np.logspace(log10(q[-1]), log10(q_max), np.ceil(n_high)+1)[1:]
330    else:
331        q_high = []
332    return np.concatenate([q_low, q, q_high])
333
334
335############################################################################
336# unit tests
337############################################################################
338import unittest
339
340
341def eval_form(q, form, pars):
342    from sasmodels import core
343    kernel = core.make_kernel(form, [q])
344    theory = core.call_kernel(kernel, pars)
345    kernel.release()
346    return theory
347
348
349def gaussian(q, q0, dq):
350    from numpy import exp, pi
351    return exp(-0.5*((q-q0)/dq)**2)/(sqrt(2*pi)*dq)
352
353
354def romberg_slit_1d(q, width, height, form, pars):
355    """
356    Romberg integration for slit resolution.
357
358    This is an adaptive integration technique.  It is called with settings
359    that make it slow to evaluate but give it good accuracy.
360    """
361    from scipy.integrate import romberg, dblquad
362
363    if any(k not in form.info['defaults'] for k in pars.keys()):
364        keys = set(form.info['defaults'].keys())
365        extra = set(pars.keys()) - keys
366        raise ValueError("bad parameters: [%s] not in [%s]"%
367                         (", ".join(sorted(extra)), ", ".join(sorted(keys))))
368
369    if np.isscalar(width):
370        width = [width]*len(q)
371    if np.isscalar(height):
372        height = [height]*len(q)
373    _int_w = lambda w, qi: eval_form(sqrt(qi**2 + w**2), form, pars)
374    _int_h = lambda h, qi: eval_form(abs(qi+h), form, pars)
375    _int_wh = lambda w, h, qi: eval_form(sqrt((qi+h)**2 + w**2), form, pars)
376    result = np.empty(len(q))
377    for i, (qi, w, h) in enumerate(zip(q, width, height)):
378        if h == 0.:
379            r = romberg(_int_w, 0, w, args=(qi,),
380                        divmax=100, vec_func=True, tol=0, rtol=1e-8)
381            result[i] = r/w
382        elif w == 0.:
383            r = romberg(_int_h, -h, h, args=(qi,),
384                        divmax=100, vec_func=True, tol=0, rtol=1e-8)
385            result[i] = r/(2*h)
386        else:
387            r, err = dblquad(_int_wh, -h, h, lambda h: 0., lambda h: width,
388                             args=(qi,))
389            result[i] = r/(w*2*h)
390
391    # r should be [float, ...], but it is [array([float]), array([float]),...]
392    return result
393
394
395def romberg_pinhole_1d(q, q_width, form, pars, nsigma=5):
396    """
397    Romberg integration for pinhole resolution.
398
399    This is an adaptive integration technique.  It is called with settings
400    that make it slow to evaluate but give it good accuracy.
401    """
402    from scipy.integrate import romberg
403
404    if any(k not in form.info['defaults'] for k in pars.keys()):
405        keys = set(form.info['defaults'].keys())
406        extra = set(pars.keys()) - keys
407        raise ValueError("bad parameters: [%s] not in [%s]"%
408                         (", ".join(sorted(extra)), ", ".join(sorted(keys))))
409
410    _fn = lambda q, q0, dq: eval_form(q, form, pars)*gaussian(q, q0, dq)
411    r = [romberg(_fn, max(qi-nsigma*dqi,1e-10*q[0]), qi+nsigma*dqi, args=(qi, dqi),
412                 divmax=100, vec_func=True, tol=0, rtol=1e-8)
413         for qi,dqi in zip(q,q_width)]
414    return np.asarray(r).flatten()
415
416
417class ResolutionTest(unittest.TestCase):
418
419    def setUp(self):
420        self.x = 0.001*np.arange(1, 11)
421        self.y = self.Iq(self.x)
422
423    def Iq(self, q):
424        "Linear function for resolution unit test"
425        return 12.0 - 1000.0*q
426
427    def test_perfect(self):
428        """
429        Perfect resolution and no smearing.
430        """
431        resolution = Perfect1D(self.x)
432        theory = self.Iq(resolution.q_calc)
433        output = resolution.apply(theory)
434        np.testing.assert_equal(output, self.y)
435
436    def test_slit_zero(self):
437        """
438        Slit smearing with perfect resolution.
439        """
440        resolution = Slit1D(self.x, width=0, height=0, q_calc=self.x)
441        theory = self.Iq(resolution.q_calc)
442        output = resolution.apply(theory)
443        np.testing.assert_equal(output, self.y)
444
445    @unittest.skip("not yet supported")
446    def test_slit_high(self):
447        """
448        Slit smearing with height 0.005
449        """
450        resolution = Slit1D(self.x, width=0, height=0.005, q_calc=self.x)
451        theory = self.Iq(resolution.q_calc)
452        output = resolution.apply(theory)
453        answer = [ 9.0618, 8.6402, 8.1187, 7.1392, 6.1528,
454                   5.5555, 4.5584, 3.5606, 2.5623, 2.0000 ]
455        np.testing.assert_allclose(output, answer, atol=1e-4)
456
457    @unittest.skip("not yet supported")
458    def test_slit_both_high(self):
459        """
460        Slit smearing with width < 100*height.
461        """
462        q = np.logspace(-4, -1, 10)
463        resolution = Slit1D(q, width=0.2, height=np.inf)
464        theory = 1000*self.Iq(resolution.q_calc**4)
465        output = resolution.apply(theory)
466        answer = [ 8.85785, 8.43012, 7.92687, 6.94566, 6.03660,
467                   5.40363, 4.40655, 3.40880, 2.41058, 2.00000 ]
468        np.testing.assert_allclose(output, answer, atol=1e-4)
469
470    @unittest.skip("not yet supported")
471    def test_slit_wide(self):
472        """
473        Slit smearing with width 0.0002
474        """
475        resolution = Slit1D(self.x, width=0.0002, height=0, q_calc=self.x)
476        theory = self.Iq(resolution.q_calc)
477        output = resolution.apply(theory)
478        answer = [ 11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0 ]
479        np.testing.assert_allclose(output, answer, atol=1e-4)
480
481    @unittest.skip("not yet supported")
482    def test_slit_both_wide(self):
483        """
484        Slit smearing with width > 100*height.
485        """
486        resolution = Slit1D(self.x, width=0.0002, height=0.000001,
487                            q_calc=self.x)
488        theory = self.Iq(resolution.q_calc)
489        output = resolution.apply(theory)
490        answer = [ 11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0 ]
491        np.testing.assert_allclose(output, answer, atol=1e-4)
492
493    def test_pinhole_zero(self):
494        """
495        Pinhole smearing with perfect resolution
496        """
497        resolution = Pinhole1D(self.x, 0.0*self.x)
498        theory = self.Iq(resolution.q_calc)
499        output = resolution.apply(theory)
500        np.testing.assert_equal(output, self.y)
501
502    def test_pinhole(self):
503        """
504        Pinhole smearing with dQ = 0.001 [Note: not dQ/Q = 0.001]
505        """
506        resolution = Pinhole1D(self.x, 0.001*np.ones_like(self.x),
507                               q_calc=self.x)
508        theory = 12.0-1000.0*resolution.q_calc
509        output = resolution.apply(theory)
510        answer = [ 10.44785079, 9.84991299, 8.98101708,
511                  7.99906585, 6.99998311, 6.00001689,
512                  5.00093415, 4.01898292, 3.15008701, 2.55214921]
513        np.testing.assert_allclose(output, answer, atol=1e-8)
514
515
516class IgorComparisonTest(unittest.TestCase):
517
518    def setUp(self):
519        self.pars = TEST_PARS_PINHOLE_SPHERE
520        from sasmodels import core
521        from sasmodels.models import sphere
522        self.model = core.load_model(sphere, dtype='double')
523
524    def Iq_sphere(self, pars, resolution):
525        from sasmodels import core
526        kernel = core.make_kernel(self.model, [resolution.q_calc])
527        theory = core.call_kernel(kernel, pars)
528        result = resolution.apply(theory)
529        kernel.release()
530        return result
531
532    def compare(self, q, output, answer, tolerance):
533        #err = (output - answer)/answer
534        #idx = abs(err) >= tolerance
535        #problem = zip(q[idx], output[idx], answer[idx], err[idx])
536        #print "\n".join(str(v) for v in problem)
537        np.testing.assert_allclose(output, answer, rtol=tolerance)
538
539    def test_perfect(self):
540        """
541        Compare sphere model with NIST Igor SANS, no resolution smearing.
542        """
543        pars = TEST_PARS_SLIT_SPHERE
544        data_string = TEST_DATA_SLIT_SPHERE
545
546        data = np.loadtxt(data_string.split('\n')).T
547        q, width, answer, _ = data
548        resolution = Perfect1D(q)
549        output = self.Iq_sphere(pars, resolution)
550        self.compare(q, output, answer, 1e-6)
551
552    def test_pinhole(self):
553        """
554        Compare pinhole resolution smearing with NIST Igor SANS
555        """
556        pars = TEST_PARS_PINHOLE_SPHERE
557        data_string = TEST_DATA_PINHOLE_SPHERE
558
559        data = np.loadtxt(data_string.split('\n')).T
560        q, q_width, answer = data
561        resolution = Pinhole1D(q, q_width)
562        output = self.Iq_sphere(pars, resolution)
563        # TODO: relative error should be lower
564        self.compare(q, output, answer, 3e-4)
565
566    def test_pinhole_romberg(self):
567        """
568        Compare pinhole resolution smearing with romberg integration result.
569        """
570        pars = TEST_PARS_PINHOLE_SPHERE
571        data_string = TEST_DATA_PINHOLE_SPHERE
572        pars['radius'] *= 5
573        radius = pars['radius']
574
575        data = np.loadtxt(data_string.split('\n')).T
576        q, q_width, answer = data
577        answer = romberg_pinhole_1d(q, q_width, self.model, pars)
578        ## Getting 0.1% requires 5 sigma and 200 points per fringe
579        #q_calc = interpolate(pinhole_extend_q(q, q_width, nsigma=5),
580        #                     2*np.pi/radius/200)
581        #tol = 0.001
582        ## The default 3 sigma and no extra points gets 1%
583        q_calc, tol = None, 0.01
584        resolution = Pinhole1D(q, q_width, q_calc=q_calc)
585        output = self.Iq_sphere(pars, resolution)
586        if 0: # debug plot
587            import matplotlib.pyplot as plt
588            resolution = Perfect1D(q)
589            source = self.Iq_sphere(pars, resolution)
590            plt.loglog(q, source, '.')
591            plt.loglog(q, answer, '-', hold=True)
592            plt.loglog(q, output, '-', hold=True)
593            plt.show()
594        self.compare(q, output, answer, tol)
595
596    def test_slit(self):
597        """
598        Compare slit resolution smearing with NIST Igor SANS
599        """
600        pars = TEST_PARS_SLIT_SPHERE
601        data_string = TEST_DATA_SLIT_SPHERE
602
603        data = np.loadtxt(data_string.split('\n')).T
604        q, delta_qv, _, answer = data
605        resolution = Slit1D(q, width=delta_qv, height=0)
606        output = self.Iq_sphere(pars, resolution)
607        # TODO: eliminate Igor test since it is too inaccurate to be useful.
608        # This means we can eliminate the test data as well, and instead
609        # use a generated q vector.
610        self.compare(q, output, answer, 0.5)
611
612    def test_slit_romberg(self):
613        """
614        Compare slit resolution smearing with romberg integration result.
615        """
616        pars = TEST_PARS_SLIT_SPHERE
617        data_string = TEST_DATA_SLIT_SPHERE
618        radius = pars['radius']
619
620        data = np.loadtxt(data_string.split('\n')).T
621        q, delta_qv, _, answer = data
622        answer = romberg_slit_1d(q, delta_qv, 0., self.model, pars)
623        q_calc = slit_extend_q(interpolate(q, 2*np.pi/radius/20),
624                               delta_qv[0], 0.)
625        resolution = Slit1D(q, width=delta_qv, height=0, q_calc=q_calc)
626        output = self.Iq_sphere(pars, resolution)
627        # TODO: relative error should be lower
628        self.compare(q, output, answer, 0.025)
629
630    def test_ellipsoid(self):
631        """
632        Compare romberg integration for ellipsoid model.
633        """
634        from .core import load_model
635        pars = {
636            'scale':0.05,
637            'rpolar':500, 'requatorial':15000,
638            'sld':6, 'solvent_sld': 1,
639            }
640        form = load_model('ellipsoid', dtype='double')
641        q = np.logspace(log10(4e-5),log10(2.5e-2), 68)
642        width, height = 0.117, 0.
643        resolution = Slit1D(q, width=width, height=height)
644        answer = romberg_slit_1d(q, width, height, form, pars)
645        output = resolution.apply(eval_form(resolution.q_calc, form, pars))
646        # TODO: 10% is too much error; use better algorithm
647        #print np.max(abs(answer-output)/answer)
648        self.compare(q, output, answer, 0.1)
649
650    #TODO: can sas q spacing be too sparse for the resolution calculation?
651    @unittest.skip("suppress sparse data test; not supported by current code")
652    def test_pinhole_sparse(self):
653        """
654        Compare pinhole resolution smearing with NIST Igor SANS on sparse data
655        """
656        pars = TEST_PARS_PINHOLE_SPHERE
657        data_string = TEST_DATA_PINHOLE_SPHERE
658
659        data = np.loadtxt(data_string.split('\n')).T
660        q, q_width, answer = data[:, ::20] # Take every nth point
661        resolution = Pinhole1D(q, q_width)
662        output = self.Iq_sphere(pars, resolution)
663        self.compare(q, output, answer, 1e-6)
664
665
666# pinhole sphere parameters
667TEST_PARS_PINHOLE_SPHERE = {
668    'scale': 1.0, 'background': 0.01,
669    'radius': 60.0, 'sld': 1, 'solvent_sld': 6.3,
670    }
671# Q, dQ, I(Q) calculated by NIST Igor SANS package
672TEST_DATA_PINHOLE_SPHERE = """\
6730.001278 0.0002847 2538.41176383
6740.001562 0.0002905 2536.91820405
6750.001846 0.0002956 2535.13182479
6760.002130 0.0003017 2533.06217813
6770.002414 0.0003087 2530.70378586
6780.002698 0.0003165 2528.05024192
6790.002982 0.0003249 2525.10408349
6800.003266 0.0003340 2521.86667499
6810.003550 0.0003437 2518.33907750
6820.003834 0.0003539 2514.52246995
6830.004118 0.0003646 2510.41798319
6840.004402 0.0003757 2506.02690988
6850.004686 0.0003872 2501.35067884
6860.004970 0.0003990 2496.38678318
6870.005253 0.0004112 2491.16237596
6880.005537 0.0004237 2485.63911673
6890.005821 0.0004365 2479.83657083
6900.006105 0.0004495 2473.75676948
6910.006389 0.0004628 2467.40145990
6920.006673 0.0004762 2460.77293372
6930.006957 0.0004899 2453.86724627
6940.007241 0.0005037 2446.69623838
6950.007525 0.0005177 2439.25775219
6960.007809 0.0005318 2431.55421398
6970.008093 0.0005461 2423.58785521
6980.008377 0.0005605 2415.36158137
6990.008661 0.0005750 2406.87009473
7000.008945 0.0005896 2398.12841186
7010.009229 0.0006044 2389.13360806
7020.009513 0.0006192 2379.88958042
7030.009797 0.0006341 2370.39776774
7040.010080 0.0006491 2360.69528793
7050.010360 0.0006641 2350.85169027
7060.010650 0.0006793 2340.42023633
7070.010930 0.0006945 2330.11206013
7080.011220 0.0007097 2319.20109972
7090.011500 0.0007251 2308.43503981
7100.011780 0.0007404 2297.44820179
7110.012070 0.0007558 2285.83853677
7120.012350 0.0007713 2274.41290746
7130.012640 0.0007868 2262.36219581
7140.012920 0.0008024 2250.51169731
7150.013200 0.0008180 2238.45596231
7160.013490 0.0008336 2225.76495666
7170.013770 0.0008493 2213.29618391
7180.014060 0.0008650 2200.19110751
7190.014340 0.0008807 2187.34050325
7200.014620 0.0008965 2174.30529864
7210.014910 0.0009123 2160.61632548
7220.015190 0.0009281 2147.21038112
7230.015470 0.0009440 2133.62023580
7240.015760 0.0009598 2119.37907426
7250.016040 0.0009757 2105.45234903
7260.016330 0.0009916 2090.86319102
7270.016610 0.0010080 2076.60576032
7280.016890 0.0010240 2062.19214565
7290.017180 0.0010390 2047.10550219
7300.017460 0.0010550 2032.38715621
7310.017740 0.0010710 2017.52560123
7320.018030 0.0010880 2001.99124318
7330.018310 0.0011040 1986.84662060
7340.018600 0.0011200 1971.03389745
7350.018880 0.0011360 1955.61395119
7360.019160 0.0011520 1940.08291563
7370.019450 0.0011680 1923.87672225
7380.019730 0.0011840 1908.10656374
7390.020020 0.0012000 1891.66297192
7400.020300 0.0012160 1875.66789021
7410.020580 0.0012320 1859.56357196
7420.020870 0.0012490 1842.79468290
7430.021150 0.0012650 1826.50064489
7440.021430 0.0012810 1810.11533702
7450.021720 0.0012970 1793.06840882
7460.022000 0.0013130 1776.51153580
7470.022280 0.0013290 1759.87201249
7480.022570 0.0013460 1742.57354412
7490.022850 0.0013620 1725.79397319
7500.023140 0.0013780 1708.35831550
7510.023420 0.0013940 1691.45256069
7520.023700 0.0014110 1674.48561783
7530.023990 0.0014270 1656.86525366
7540.024270 0.0014430 1639.79847285
7550.024550 0.0014590 1622.68887088
7560.024840 0.0014760 1604.96421100
7570.025120 0.0014920 1587.85768129
7580.025410 0.0015080 1569.99297335
7590.025690 0.0015240 1552.84580279
7600.025970 0.0015410 1535.54074115
7610.026260 0.0015570 1517.75249337
7620.026540 0.0015730 1500.40115023
7630.026820 0.0015900 1483.03632237
7640.027110 0.0016060 1465.05942429
7650.027390 0.0016220 1447.67682181
7660.027670 0.0016390 1430.46495191
7670.027960 0.0016550 1412.49232282
7680.028240 0.0016710 1395.13182318
7690.028520 0.0016880 1377.93439837
7700.028810 0.0017040 1359.99528971
7710.029090 0.0017200 1342.67274512
7720.029370 0.0017370 1325.55375609
773"""
774
775# Slit sphere parameters
776TEST_PARS_SLIT_SPHERE = {
777    'scale': 0.01, 'background': 0.01,
778    'radius': 60000, 'sld': 1, 'solvent_sld': 4,
779    }
780# Q dQ I(Q) I_smeared(Q)
781TEST_DATA_SLIT_SPHERE = """\
7822.26097e-05 0.117 5.5781372896e+09 1.4626077708e+06
7832.53847e-05 0.117 5.0363141458e+09 1.3117318023e+06
7842.81597e-05 0.117 4.4850108103e+09 1.1594863713e+06
7853.09347e-05 0.117 3.9364658459e+09 1.0094881630e+06
7863.37097e-05 0.117 3.4019975074e+09 8.6518941303e+05
7873.92597e-05 0.117 2.4139519814e+09 6.0232158311e+05
7884.48097e-05 0.117 1.5816877820e+09 3.8739994090e+05
7895.03597e-05 0.117 9.3715407224e+08 2.2745304775e+05
7905.59097e-05 0.117 4.8387917428e+08 1.2101295768e+05
7916.14597e-05 0.117 2.0193586928e+08 6.0055107771e+04
7926.70097e-05 0.117 5.5886110911e+07 3.2749521065e+04
7937.25597e-05 0.117 3.7782348010e+06 2.6350963616e+04
7947.81097e-05 0.117 5.3407817904e+06 2.9624963314e+04
7958.36597e-05 0.117 2.7975485523e+07 3.4403962254e+04
7968.92097e-05 0.117 4.9845448282e+07 3.6130017903e+04
7979.47597e-05 0.117 6.0092588905e+07 3.3495107849e+04
7981.00310e-04 0.117 5.6823430831e+07 2.7475726279e+04
7991.05860e-04 0.117 4.3857024036e+07 2.0144282226e+04
8001.11410e-04 0.117 2.7277144760e+07 1.3647403260e+04
8011.22510e-04 0.117 3.3119334113e+06 6.6519711526e+03
8021.33610e-04 0.117 1.4412859402e+06 6.9726212813e+03
8031.44710e-04 0.117 8.5620162463e+06 8.1441335775e+03
8041.55810e-04 0.117 9.6957429033e+06 6.4559996521e+03
8051.66910e-04 0.117 4.3818341914e+06 3.6252154156e+03
8061.78010e-04 0.117 2.7448997387e+05 2.4006505342e+03
8071.89110e-04 0.117 8.0472009936e+05 2.8187789089e+03
8082.00210e-04 0.117 2.8149552834e+06 3.0915662855e+03
8092.11310e-04 0.117 2.7510907861e+06 2.3722530293e+03
8102.22410e-04 0.117 1.0053133293e+06 1.4473468311e+03
8112.33510e-04 0.117 5.8428305052e+03 1.2048540556e+03
8122.44610e-04 0.117 5.1699305004e+05 1.4625670042e+03
8132.55710e-04 0.117 1.2120227268e+06 1.5010705973e+03
8142.66810e-04 0.117 9.7896842846e+05 1.1336343426e+03
8152.77910e-04 0.117 2.5507264791e+05 8.1848818080e+02
8163.05660e-04 0.117 5.2403101181e+05 7.4913374357e+02
8173.33410e-04 0.117 5.8699343809e+04 4.4669964560e+02
8183.61160e-04 0.117 3.0844327150e+05 4.6774007542e+02
8193.88910e-04 0.117 8.3360142970e+03 2.7169550220e+02
8204.16660e-04 0.117 1.8630080583e+05 3.0710983679e+02
8214.44410e-04 0.117 3.1616804732e-01 1.7959006831e+02
8224.72160e-04 0.117 1.1299016314e+05 2.0763952339e+02
8234.99910e-04 0.117 2.9952522747e+03 1.2536542765e+02
8245.27660e-04 0.117 6.7625695649e+04 1.4013969777e+02
8255.55410e-04 0.117 7.6927460089e+03 8.2145593180e+01
8266.10910e-04 0.117 1.1229057779e+04 8.4519745643e+01
8276.66410e-04 0.117 1.3035567943e+04 8.1554625609e+01
8287.21910e-04 0.117 1.3309931343e+04 7.4437319172e+01
8297.77410e-04 0.117 1.2462626212e+04 6.4697088261e+01
8308.32910e-04 0.117 1.0912927143e+04 5.3773301044e+01
8318.88410e-04 0.117 9.0172597469e+03 4.2843375753e+01
8329.43910e-04 0.117 7.0496495917e+03 3.2771032724e+01
8339.99410e-04 0.117 5.2030483682e+03 2.4113557144e+01
8341.05491e-03 0.117 3.5988976711e+03 1.7160773658e+01
8351.11041e-03 0.117 2.2996060652e+03 1.2016626459e+01
8361.22141e-03 0.117 6.4766590598e+02 6.0373017740e+00
8371.33241e-03 0.117 4.1963483264e+01 4.5215452974e+00
8381.44341e-03 0.117 6.3370708246e+01 5.1054681903e+00
8391.55441e-03 0.117 3.0736750577e+02 5.9176165298e+00
8401.66541e-03 0.117 5.0327682399e+02 5.9815000189e+00
8411.77641e-03 0.117 5.4084331454e+02 5.1634639625e+00
8421.88741e-03 0.117 4.3488671756e+02 3.8535158148e+00
8431.99841e-03 0.117 2.6322287860e+02 2.5824997753e+00
8442.10941e-03 0.117 1.0793633150e+02 1.7315517194e+00
8452.22041e-03 0.117 1.8474448850e+01 1.4077213604e+00
8462.33141e-03 0.117 1.5864062279e+00 1.4771560682e+00
8472.44241e-03 0.117 3.2267213848e+01 1.6916253448e+00
8482.55341e-03 0.117 7.4289116207e+01 1.8274751193e+00
8492.66441e-03 0.117 9.9000521929e+01 1.7706812289e+00
850"""
851
852def main():
853    """
854    Run tests given is sys.argv.
855
856    Returns 0 if success or 1 if any tests fail.
857    """
858    import sys
859    import xmlrunner
860
861    suite = unittest.TestSuite()
862    suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]))
863
864    runner = xmlrunner.XMLTestRunner(output='logs')
865    result = runner.run(suite)
866    return 1 if result.failures or result.errors else 0
867
868
869############################################################################
870# usage demo
871############################################################################
872
873def _eval_demo_1d(resolution, title):
874    import sys
875    from sasmodels import core
876    name = sys.argv[1] if len(sys.argv) > 1 else 'cylinder'
877
878    if name == 'cylinder':
879        pars = {'length':210, 'radius':500}
880    elif name == 'teubner_strey':
881        pars = {'a2':0.003, 'c1':-1e4, 'c2':1e10, 'background':0.312643}
882    elif name == 'sphere' or name == 'spherepy':
883        pars = TEST_PARS_SLIT_SPHERE
884    elif name == 'ellipsoid':
885        pars = {
886            'scale':0.05,
887            'rpolar':500, 'requatorial':15000,
888            'sld':6, 'solvent_sld': 1,
889            }
890    else:
891        pars = {}
892    defn = core.load_model_definition(name)
893    model = core.load_model(defn)
894
895    kernel = core.make_kernel(model, [resolution.q_calc])
896    theory = core.call_kernel(kernel, pars)
897    Iq = resolution.apply(theory)
898
899    if isinstance(resolution, Slit1D):
900        width, height = resolution.width, resolution.height
901        Iq_romb = romberg_slit_1d(resolution.q, width, height, model, pars)
902    else:
903        dq = resolution.q_width
904        Iq_romb = romberg_pinhole_1d(resolution.q, dq, model, pars)
905
906    import matplotlib.pyplot as plt
907    plt.loglog(resolution.q_calc, theory, label='unsmeared')
908    plt.loglog(resolution.q, Iq, label='smeared', hold=True)
909    plt.loglog(resolution.q, Iq_romb, label='romberg smeared', hold=True)
910    plt.legend()
911    plt.title(title)
912    plt.xlabel("Q (1/Ang)")
913    plt.ylabel("I(Q) (1/cm)")
914
915def demo_pinhole_1d():
916    q = np.logspace(-4, np.log10(0.2), 400)
917    q_width = 0.1*q
918    resolution = Pinhole1D(q, q_width)
919    _eval_demo_1d(resolution, title="10% dQ/Q Pinhole Resolution")
920
921def demo_slit_1d():
922    q = np.logspace(-4, np.log10(0.2), 400)
923    w = h = 0.
924    #w = 0.005
925    w = 0.0277790
926    #h = 0.00277790
927    resolution = Slit1D(q, w, h)
928    _eval_demo_1d(resolution, title="(%g,%g) Slit Resolution"%(w,h))
929
930def demo():
931    import matplotlib.pyplot as plt
932    plt.subplot(121)
933    demo_pinhole_1d()
934    #plt.yscale('linear')
935    plt.subplot(122)
936    demo_slit_1d()
937    #plt.yscale('linear')
938    plt.show()
939
940
941if __name__ == "__main__":
942    #demo()
943    main()
Note: See TracBrowser for help on using the repository browser.