source: sasmodels/sasmodels/resolution.py @ fdc538a

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

delint

  • Property mode set to 100644
File size: 38.0 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
7
8__all__ = ["Resolution", "Perfect1D", "Pinhole1D", "Slit1D",
9           "apply_resolution_matrix", "pinhole_resolution", "slit_resolution",
10           "pinhole_extend_q", "slit_extend_q", "bin_edges",
11           "interpolate", "linear_extrapolation", "geometric_extrapolation",
12           ]
13
14from scipy.special import erf
15from numpy import sqrt, log, log10
16import numpy as np
17
18MINIMUM_RESOLUTION = 1e-8
19
20
21# When extrapolating to -q, what is the minimum positive q relative to q_min
22# that we wish to calculate?
23MIN_Q_SCALE_FOR_NEGATIVE_Q_EXTRAPOLATION = 0.01
24
25class Resolution(object):
26    """
27    Abstract base class defining a 1D resolution function.
28
29    *q* is the set of q values at which the data is measured.
30
31    *q_calc* is the set of q values at which the theory needs to be evaluated.
32    This may extend and interpolate the q values.
33
34    *apply* is the method to call with I(q_calc) to compute the resolution
35    smeared theory I(q).
36    """
37    q = None
38    q_calc = None
39    def apply(self, theory):
40        """
41        Smear *theory* by the resolution function, returning *Iq*.
42        """
43        raise NotImplementedError("Subclass does not define the apply function")
44
45
46class Perfect1D(Resolution):
47    """
48    Resolution function to use when there is no actual resolution smearing
49    to be applied.  It has the same interface as the other resolution
50    functions, but returns the identity function.
51    """
52    def __init__(self, q):
53        self.q_calc = self.q = q
54
55    def apply(self, theory):
56        return theory
57
58
59class Pinhole1D(Resolution):
60    r"""
61    Pinhole aperture with q-dependent gaussian resolution.
62
63    *q* points at which the data is measured.
64
65    *q_width* gaussian 1-sigma resolution at each data point.
66
67    *q_calc* is the list of points to calculate, or None if this should
68    be estimated from the *q* and *q_width*.
69    """
70    def __init__(self, q, q_width, q_calc=None, nsigma=3):
71        #*min_step* is the minimum point spacing to use when computing the
72        #underlying model.  It should be on the order of
73        #$\tfrac{1}{10}\tfrac{2\pi}{d_\text{max}}$ to make sure that fringes
74        #are computed with sufficient density to avoid aliasing effects.
75
76        # Protect against calls with q_width=0.  The extend_q function will
77        # not extend the q if q_width is 0, but q_width must be non-zero when
78        # constructing the weight matrix to avoid division by zero errors.
79        # In practice this should never be needed, since resolution should
80        # default to Perfect1D if the pinhole geometry is not defined.
81        self.q, self.q_width = q, q_width
82        self.q_calc = pinhole_extend_q(q, q_width, nsigma=nsigma) \
83            if q_calc is None else np.sort(q_calc)
84        self.weight_matrix = pinhole_resolution(self.q_calc,
85                self.q, np.maximum(q_width, MINIMUM_RESOLUTION))
86
87    def apply(self, theory):
88        return apply_resolution_matrix(self.weight_matrix, theory)
89
90
91class Slit1D(Resolution):
92    """
93    Slit aperture with a complicated resolution function.
94
95    *q* points at which the data is measured.
96
97    *qx_width* slit width
98
99    *qy_height* slit height
100
101    *q_calc* is the list of points to calculate, or None if this should
102    be estimated from the *q* and *q_width*.
103
104    The *weight_matrix* is computed by :func:`slit1d_resolution`
105    """
106    def __init__(self, q, width, height=0., q_calc=None):
107        # Remember what width/height was used even though we won't need them
108        # after the weight matrix is constructed
109        self.width, self.height = width, height
110
111        # Allow independent resolution on each point even though it is not
112        # needed in practice.
113        if np.isscalar(width):
114            width = np.ones(len(q))*width
115        else:
116            width = np.asarray(width)
117        if np.isscalar(height):
118            height = np.ones(len(q))*height
119        else:
120            height = np.asarray(height)
121
122        self.q = q.flatten()
123        self.q_calc = slit_extend_q(q, width, height) \
124            if q_calc is None else np.sort(q_calc)
125        self.weight_matrix = \
126            slit_resolution(self.q_calc, self.q, width, height)
127
128    def apply(self, theory):
129        return apply_resolution_matrix(self.weight_matrix, theory)
130
131
132def apply_resolution_matrix(weight_matrix, theory):
133    """
134    Apply the resolution weight matrix to the computed theory function.
135    """
136    #print("apply shapes", theory.shape, weight_matrix.shape)
137    Iq = np.dot(theory[None, :], weight_matrix)
138    #print("result shape",Iq.shape)
139    return Iq.flatten()
140
141
142def pinhole_resolution(q_calc, q, q_width):
143    """
144    Compute the convolution matrix *W* for pinhole resolution 1-D data.
145
146    Each row *W[i]* determines the normalized weight that the corresponding
147    points *q_calc* contribute to the resolution smeared point *q[i]*.  Given
148    *W*, the resolution smearing can be computed using *dot(W,q)*.
149
150    *q_calc* must be increasing.  *q_width* must be greater than zero.
151    """
152    # The current algorithm is a midpoint rectangle rule.  In the test case,
153    # neither trapezoid nor Simpson's rule improved the accuracy.
154    edges = bin_edges(q_calc)
155    edges[edges < 0.0] = 0.0 # clip edges below zero
156    G = erf((edges[:, None] - q[None, :]) / (sqrt(2.0)*q_width)[None, :])
157    weights = G[1:] - G[:-1]
158    weights /= np.sum(weights, axis=0)[None, :]
159    return weights
160
161
162def slit_resolution(q_calc, q, width, height, n_height=30):
163    r"""
164    Build a weight matrix to compute *I_s(q)* from *I(q_calc)*, given
165    $q_\perp$ = *width* and $q_\parallel$ = *height*.  *n_height* is
166    is the number of steps to use in the integration over $q_\parallel$
167    when both $q_\perp$ and $q_\parallel$ are non-zero.
168
169    Each $q$ can have an independent width and height value even though
170    current instruments use the same slit setting for all measured points.
171
172    If slit height is large relative to width, use:
173
174    .. math::
175
176        I_s(q_i) = \frac{1}{\Delta q_\perp}
177            \int_0^{\Delta q_\perp}
178                I\left(\sqrt{q_i^2 + q_\perp^2}\right) \,dq_\perp
179
180    If slit width is large relative to height, use:
181
182    .. math::
183
184        I_s(q_i) = \frac{1}{2 \Delta q_\parallel}
185            \int_{-\Delta q_\parallel}^{\Delta q_\parallel}
186                I\left(|q_i + q_\parallel|\right) \,dq_\parallel
187
188    For a mixture of slit width and height use:
189
190    .. math::
191
192        I_s(q_i) = \frac{1}{2 \Delta q_\parallel \Delta q_\perp}
193            \int_{-\Delta q_\parallel}^{\Delta q_\parallel}
194            \int_0^{\Delta q_\perp}
195                I\left(\sqrt{(q_i + q_\parallel)^2 + q_\perp^2}\right)
196                \,dq_\perp dq_\parallel
197
198
199    **Algorithm**
200
201    We are using the mid-point integration rule to assign weights to each
202    element of a weight matrix $W$ so that
203
204    .. math::
205
206        I_s(q) = W\,I(q_\text{calc})
207
208    If *q_calc* is at the mid-point, we can infer the bin edges from the
209    pairwise averages of *q_calc*, adding the missing edges before
210    *q_calc[0]* and after *q_calc[-1]*.
211
212    For $q_\parallel = 0$, the smeared value can be computed numerically
213    using the $u$ substitution
214
215    .. math::
216
217        u_j = \sqrt{q_j^2 - q^2}
218
219    This gives
220
221    .. math::
222
223        I_s(q) \approx \sum_j I(u_j) \Delta u_j
224
225    where $I(u_j)$ is the value at the mid-point, and $\Delta u_j$ is the
226    difference between consecutive edges which have been first converted
227    to $u$.  Only $u_j \in [0, \Delta q_\perp]$ are used, which corresponds
228    to $q_j \in \left[q, \sqrt{q^2 + \Delta q_\perp}\right]$, so
229
230    .. math::
231
232        W_{ij} = \frac{1}{\Delta q_\perp} \Delta u_j
233               = \frac{1}{\Delta q_\perp} \left(
234                    \sqrt{q_{j+1}^2 - q_i^2} - \sqrt{q_j^2 - q_i^2} \right)
235            \ \text{if}\  q_j \in \left[q_i, \sqrt{q_i^2 + q_\perp^2}\right]
236
237    where $I_s(q_i)$ is the theory function being computed and $q_j$ are the
238    mid-points between the calculated values in *q_calc*.  We tweak the
239    edges of the initial and final intervals so that they lie on integration
240    limits.
241
242    (To be precise, the transformed midpoint $u(q_j)$ is not necessarily the
243    midpoint of the edges $u((q_{j-1}+q_j)/2)$ and $u((q_j + q_{j+1})/2)$,
244    but it is at least in the interval, so the approximation is going to be
245    a little better than the left or right Riemann sum, and should be
246    good enough for our purposes.)
247
248    For $q_\perp = 0$, the $u$ substitution is simpler:
249
250    .. math::
251
252        u_j = \left|q_j - q\right|
253
254    so
255
256    .. math::
257
258        W_{ij} = \frac{1}{2 \Delta q_\parallel} \Delta u_j
259            = \frac{1}{2 \Delta q_\parallel} (q_{j+1} - q_j)
260            \ \text{if}\ q_j \in
261                \left[q-\Delta q_\parallel, q+\Delta q_\parallel\right]
262
263    However, we need to support cases were $u_j < 0$, which means using
264    $2 (q_{j+1} - q_j)$ when $q_j \in \left[0, q_\parallel-q_i\right]$.
265    This is not an issue for $q_i > q_\parallel$.
266
267    For both $q_\perp > 0$ and $q_\parallel > 0$ we perform a 2 dimensional
268    integration with
269
270    .. math::
271
272        u_{jk} = \sqrt{q_j^2 - (q + (k\Delta q_\parallel/L))^2}
273            \ \text{for}\ k = -L \ldots L
274
275    for $L$ = *n_height*.  This gives
276
277    .. math::
278
279        W_{ij} = \frac{1}{2 \Delta q_\perp q_\parallel}
280            \sum_{k=-L}^L \Delta u_{jk}
281                \left(\frac{\Delta q_\parallel}{2 L + 1}\right)
282
283
284    """
285    #np.set_printoptions(precision=6, linewidth=10000)
286
287    # The current algorithm is a midpoint rectangle rule.
288    q_edges = bin_edges(q_calc) # Note: requires q > 0
289    q_edges[q_edges < 0.0] = 0.0 # clip edges below zero
290    weights = np.zeros((len(q), len(q_calc)), 'd')
291
292    #print(q_calc)
293    for i, (qi, w, h) in enumerate(zip(q, width, height)):
294        if w == 0. and h == 0.:
295            # Perfect resolution, so return the theory value directly.
296            # Note: assumes that q is a subset of q_calc.  If qi need not be
297            # in q_calc, then we can do a weighted interpolation by looking
298            # up qi in q_calc, then weighting the result by the relative
299            # distance to the neighbouring points.
300            weights[i, :] = (q_calc == qi)
301        elif h == 0:
302            weights[i, :] = _q_perp_weights(q_edges, qi, w)
303        elif w == 0:
304            in_x = 1.0 * ((q_calc >= qi-h) & (q_calc <= qi+h))
305            abs_x = 1.0*(q_calc < abs(qi - h)) if qi < h else 0.
306            #print(qi - h, qi + h)
307            #print(in_x + abs_x)
308            weights[i, :] = (in_x + abs_x) * np.diff(q_edges) / (2*h)
309        else:
310            L = n_height
311            for k in range(-L, L+1):
312                weights[i, :] += _q_perp_weights(q_edges, qi+k*h/L, w)
313            weights[i, :] /= 2*L + 1
314
315    return weights.T
316
317
318def _q_perp_weights(q_edges, qi, w):
319    # Convert bin edges from q to u
320    u_limit = np.sqrt(qi**2 + w**2)
321    u_edges = q_edges**2 - qi**2
322    u_edges[q_edges < abs(qi)] = 0.
323    u_edges[q_edges > u_limit] = u_limit**2 - qi**2
324    weights = np.diff(np.sqrt(u_edges))/w
325    #print("i, qi",i,qi,qi+width)
326    #print(q_calc)
327    #print(weights)
328    return weights
329
330
331def pinhole_extend_q(q, q_width, nsigma=3):
332    """
333    Given *q* and *q_width*, find a set of sampling points *q_calc* so
334    that each point $I(q)$ has sufficient support from the underlying
335    function.
336    """
337    q_min, q_max = np.min(q - nsigma*q_width), np.max(q + nsigma*q_width)
338    return linear_extrapolation(q, q_min, q_max)
339
340
341def slit_extend_q(q, width, height):
342    """
343    Given *q*, *width* and *height*, find a set of sampling points *q_calc* so
344    that each point I(q) has sufficient support from the underlying
345    function.
346    """
347    q_min, q_max = np.min(q-height), np.max(np.sqrt((q+height)**2 + width**2))
348
349    return geometric_extrapolation(q, q_min, q_max)
350
351
352def bin_edges(x):
353    """
354    Determine bin edges from bin centers, assuming that edges are centered
355    between the bins.
356
357    Note: this uses the arithmetic mean, which may not be appropriate for
358    log-scaled data.
359    """
360    if len(x) < 2 or (np.diff(x) < 0).any():
361        raise ValueError("Expected bins to be an increasing set")
362    edges = np.hstack([
363        x[0]  - 0.5*(x[1]  - x[0]),  # first point minus half first interval
364        0.5*(x[1:] + x[:-1]),        # mid points of all central intervals
365        x[-1] + 0.5*(x[-1] - x[-2]), # last point plus half last interval
366        ])
367    return edges
368
369
370def interpolate(q, max_step):
371    """
372    Returns *q_calc* with points spaced at most max_step apart.
373    """
374    step = np.diff(q)
375    index = step > max_step
376    if np.any(index):
377        inserts = []
378        for q_i, step_i in zip(q[:-1][index], step[index]):
379            n = np.ceil(step_i/max_step)
380            inserts.extend(q_i + np.arange(1, n)*(step_i/n))
381        # Extend a couple of fringes beyond the end of the data
382        inserts.extend(q[-1] + np.arange(1, 8)*max_step)
383        q_calc = np.sort(np.hstack((q, inserts)))
384    else:
385        q_calc = q
386    return q_calc
387
388
389def linear_extrapolation(q, q_min, q_max):
390    """
391    Extrapolate *q* out to [*q_min*, *q_max*] using the step size in *q* as
392    a guide.  Extrapolation below uses about the same size as the first
393    interval.  Extrapolation above uses about the same size as the final
394    interval.
395
396    if *q_min* is zero or less then *q[0]/10* is used instead.
397    """
398    q = np.sort(q)
399    if q_min < q[0]:
400        if q_min <= 0: q_min = q_min*MIN_Q_SCALE_FOR_NEGATIVE_Q_EXTRAPOLATION
401        n_low = np.ceil((q[0]-q_min) / (q[1]-q[0])) if q[1] > q[0] else 15
402        q_low = np.linspace(q_min, q[0], n_low+1)[:-1]
403    else:
404        q_low = []
405    if q_max > q[-1]:
406        n_high = np.ceil((q_max-q[-1]) / (q[-1]-q[-2])) if q[-1] > q[-2] else 15
407        q_high = np.linspace(q[-1], q_max, n_high+1)[1:]
408    else:
409        q_high = []
410    return np.concatenate([q_low, q, q_high])
411
412
413def geometric_extrapolation(q, q_min, q_max, points_per_decade=None):
414    r"""
415    Extrapolate *q* to [*q_min*, *q_max*] using geometric steps, with the
416    average geometric step size in *q* as the step size.
417
418    if *q_min* is zero or less then *q[0]/10* is used instead.
419
420    *points_per_decade* sets the ratio between consecutive steps such
421    that there will be $n$ points used for every factor of 10 increase
422    in *q*.
423
424    If *points_per_decade* is not given, it will be estimated as follows.
425    Starting at $q_1$ and stepping geometrically by $\Delta q$ to $q_n$
426    in $n$ points gives a geometric average of:
427
428    .. math::
429
430         \log \Delta q = (\log q_n - log q_1) / (n - 1)
431
432    From this we can compute the number of steps required to extend $q$
433    from $q_n$ to $q_\text{max}$ by $\Delta q$ as:
434
435    .. math::
436
437         n_\text{extend} = (\log q_\text{max} - \log q_n) / \log \Delta q
438
439    Substituting:
440
441    .. math::
442
443        n_\text{extend} = (n-1) (\log q_\text{max} - \log q_n)
444            / (\log q_n - log q_1)
445    """
446    q = np.sort(q)
447    if points_per_decade is None:
448        log_delta_q = (len(q) - 1) / (log(q[-1]) - log(q[0]))
449    else:
450        log_delta_q = log(10.) / points_per_decade
451    if q_min < q[0]:
452        if q_min < 0: q_min = q[0]*MIN_Q_SCALE_FOR_NEGATIVE_Q_EXTRAPOLATION
453        n_low = log_delta_q * (log(q[0])-log(q_min))
454        q_low = np.logspace(log10(q_min), log10(q[0]), np.ceil(n_low)+1)[:-1]
455    else:
456        q_low = []
457    if q_max > q[-1]:
458        n_high = log_delta_q * (log(q_max)-log(q[-1]))
459        q_high = np.logspace(log10(q[-1]), log10(q_max), np.ceil(n_high)+1)[1:]
460    else:
461        q_high = []
462    return np.concatenate([q_low, q, q_high])
463
464
465############################################################################
466# unit tests
467############################################################################
468import unittest
469
470
471def eval_form(q, form, pars):
472    from sasmodels import core
473    kernel = core.make_kernel(form, [q])
474    theory = core.call_kernel(kernel, pars)
475    kernel.release()
476    return theory
477
478
479def gaussian(q, q0, dq):
480    from numpy import exp, pi
481    return exp(-0.5*((q-q0)/dq)**2)/(sqrt(2*pi)*dq)
482
483
484def romberg_slit_1d(q, width, height, form, pars):
485    """
486    Romberg integration for slit resolution.
487
488    This is an adaptive integration technique.  It is called with settings
489    that make it slow to evaluate but give it good accuracy.
490    """
491    from scipy.integrate import romberg
492
493    if any(k not in form.info['defaults'] for k in pars.keys()):
494        keys = set(form.info['defaults'].keys())
495        extra = set(pars.keys()) - keys
496        raise ValueError("bad parameters: [%s] not in [%s]"%
497                         (", ".join(sorted(extra)), ", ".join(sorted(keys))))
498
499    if np.isscalar(width):
500        width = [width]*len(q)
501    if np.isscalar(height):
502        height = [height]*len(q)
503    _int_w = lambda w, qi: eval_form(sqrt(qi**2 + w**2), form, pars)
504    _int_h = lambda h, qi: eval_form(abs(qi+h), form, pars)
505    # If both width and height are defined, then it is too slow to use dblquad.
506    # Instead use trapz on a fixed grid, interpolated into the I(Q) for
507    # the extended Q range.
508    #_int_wh = lambda w, h, qi: eval_form(sqrt((qi+h)**2 + w**2), form, pars)
509    q_calc = slit_extend_q(q, np.asarray(width), np.asarray(height))
510    Iq = eval_form(q_calc, form, pars)
511    result = np.empty(len(q))
512    for i, (qi, w, h) in enumerate(zip(q, width, height)):
513        if h == 0.:
514            r = romberg(_int_w, 0, w, args=(qi,),
515                        divmax=100, vec_func=True, tol=0, rtol=1e-8)
516            result[i] = r/w
517        elif w == 0.:
518            r = romberg(_int_h, -h, h, args=(qi,),
519                        divmax=100, vec_func=True, tol=0, rtol=1e-8)
520            result[i] = r/(2*h)
521        else:
522            w_grid = np.linspace(0, w, 21)[None, :]
523            h_grid = np.linspace(-h, h, 23)[:, None]
524            u = sqrt((qi+h_grid)**2 + w_grid**2)
525            Iu = np.interp(u, q_calc, Iq)
526            #print(np.trapz(Iu, w_grid, axis=1))
527            Is = np.trapz(np.trapz(Iu, w_grid, axis=1), h_grid[:, 0])
528            result[i] = Is / (2*h*w)
529            # from scipy.integrate import dblquad
530            # r, err = dblquad(_int_wh, -h, h, lambda h: 0., lambda h: w,
531            #                  args=(qi,))
532            # result[i] = r/(w*2*h)
533
534    # r should be [float, ...], but it is [array([float]), array([float]),...]
535    return result
536
537
538def romberg_pinhole_1d(q, q_width, form, pars, nsigma=5):
539    """
540    Romberg integration for pinhole resolution.
541
542    This is an adaptive integration technique.  It is called with settings
543    that make it slow to evaluate but give it good accuracy.
544    """
545    from scipy.integrate import romberg
546
547    if any(k not in form.info['defaults'] for k in pars.keys()):
548        keys = set(form.info['defaults'].keys())
549        extra = set(pars.keys()) - keys
550        raise ValueError("bad parameters: [%s] not in [%s]"%
551                         (", ".join(sorted(extra)), ", ".join(sorted(keys))))
552
553    _fn = lambda q, q0, dq: eval_form(q, form, pars)*gaussian(q, q0, dq)
554    r = [romberg(_fn, max(qi-nsigma*dqi, 1e-10*q[0]), qi+nsigma*dqi,
555                 args=(qi, dqi), divmax=100, vec_func=True, tol=0, rtol=1e-8)
556         for qi, dqi in zip(q, q_width)]
557    return np.asarray(r).flatten()
558
559
560class ResolutionTest(unittest.TestCase):
561
562    def setUp(self):
563        self.x = 0.001*np.arange(1, 11)
564        self.y = self.Iq(self.x)
565
566    def Iq(self, q):
567        "Linear function for resolution unit test"
568        return 12.0 - 1000.0*q
569
570    def test_perfect(self):
571        """
572        Perfect resolution and no smearing.
573        """
574        resolution = Perfect1D(self.x)
575        theory = self.Iq(resolution.q_calc)
576        output = resolution.apply(theory)
577        np.testing.assert_equal(output, self.y)
578
579    def test_slit_zero(self):
580        """
581        Slit smearing with perfect resolution.
582        """
583        resolution = Slit1D(self.x, width=0, height=0, q_calc=self.x)
584        theory = self.Iq(resolution.q_calc)
585        output = resolution.apply(theory)
586        np.testing.assert_equal(output, self.y)
587
588    @unittest.skip("not yet supported")
589    def test_slit_high(self):
590        """
591        Slit smearing with height 0.005
592        """
593        resolution = Slit1D(self.x, width=0, height=0.005, q_calc=self.x)
594        theory = self.Iq(resolution.q_calc)
595        output = resolution.apply(theory)
596        answer = [
597            9.0618, 8.6402, 8.1187, 7.1392, 6.1528,
598            5.5555, 4.5584, 3.5606, 2.5623, 2.0000,
599            ]
600        np.testing.assert_allclose(output, answer, atol=1e-4)
601
602    @unittest.skip("not yet supported")
603    def test_slit_both_high(self):
604        """
605        Slit smearing with width < 100*height.
606        """
607        q = np.logspace(-4, -1, 10)
608        resolution = Slit1D(q, width=0.2, height=np.inf)
609        theory = 1000*self.Iq(resolution.q_calc**4)
610        output = resolution.apply(theory)
611        answer = [
612            8.85785, 8.43012, 7.92687, 6.94566, 6.03660,
613            5.40363, 4.40655, 3.40880, 2.41058, 2.00000,
614            ]
615        np.testing.assert_allclose(output, answer, atol=1e-4)
616
617    @unittest.skip("not yet supported")
618    def test_slit_wide(self):
619        """
620        Slit smearing with width 0.0002
621        """
622        resolution = Slit1D(self.x, width=0.0002, height=0, q_calc=self.x)
623        theory = self.Iq(resolution.q_calc)
624        output = resolution.apply(theory)
625        answer = [
626            11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0,
627            ]
628        np.testing.assert_allclose(output, answer, atol=1e-4)
629
630    @unittest.skip("not yet supported")
631    def test_slit_both_wide(self):
632        """
633        Slit smearing with width > 100*height.
634        """
635        resolution = Slit1D(self.x, width=0.0002, height=0.000001,
636                            q_calc=self.x)
637        theory = self.Iq(resolution.q_calc)
638        output = resolution.apply(theory)
639        answer = [
640            11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0,
641            ]
642        np.testing.assert_allclose(output, answer, atol=1e-4)
643
644    def test_pinhole_zero(self):
645        """
646        Pinhole smearing with perfect resolution
647        """
648        resolution = Pinhole1D(self.x, 0.0*self.x)
649        theory = self.Iq(resolution.q_calc)
650        output = resolution.apply(theory)
651        np.testing.assert_equal(output, self.y)
652
653    def test_pinhole(self):
654        """
655        Pinhole smearing with dQ = 0.001 [Note: not dQ/Q = 0.001]
656        """
657        resolution = Pinhole1D(self.x, 0.001*np.ones_like(self.x),
658                               q_calc=self.x)
659        theory = 12.0-1000.0*resolution.q_calc
660        output = resolution.apply(theory)
661        answer = [
662            10.44785079, 9.84991299, 8.98101708,
663            7.99906585, 6.99998311, 6.00001689,
664            5.00093415, 4.01898292, 3.15008701, 2.55214921,
665            ]
666        np.testing.assert_allclose(output, answer, atol=1e-8)
667
668
669class IgorComparisonTest(unittest.TestCase):
670
671    def setUp(self):
672        self.pars = TEST_PARS_PINHOLE_SPHERE
673        from sasmodels import core
674        from sasmodels.models import sphere
675        self.model = core.load_model(sphere, dtype='double')
676
677    def Iq_sphere(self, pars, resolution):
678        from sasmodels import core
679        kernel = core.make_kernel(self.model, [resolution.q_calc])
680        theory = core.call_kernel(kernel, pars)
681        result = resolution.apply(theory)
682        kernel.release()
683        return result
684
685    def compare(self, q, output, answer, tolerance):
686        #err = (output - answer)/answer
687        #idx = abs(err) >= tolerance
688        #problem = zip(q[idx], output[idx], answer[idx], err[idx])
689        #print("\n".join(str(v) for v in problem))
690        np.testing.assert_allclose(output, answer, rtol=tolerance)
691
692    def test_perfect(self):
693        """
694        Compare sphere model with NIST Igor SANS, no resolution smearing.
695        """
696        pars = TEST_PARS_SLIT_SPHERE
697        data_string = TEST_DATA_SLIT_SPHERE
698
699        data = np.loadtxt(data_string.split('\n')).T
700        q, width, answer, _ = data
701        resolution = Perfect1D(q)
702        output = self.Iq_sphere(pars, resolution)
703        self.compare(q, output, answer, 1e-6)
704
705    def test_pinhole(self):
706        """
707        Compare pinhole resolution smearing with NIST Igor SANS
708        """
709        pars = TEST_PARS_PINHOLE_SPHERE
710        data_string = TEST_DATA_PINHOLE_SPHERE
711
712        data = np.loadtxt(data_string.split('\n')).T
713        q, q_width, answer = data
714        resolution = Pinhole1D(q, q_width)
715        output = self.Iq_sphere(pars, resolution)
716        # TODO: relative error should be lower
717        self.compare(q, output, answer, 3e-4)
718
719    def test_pinhole_romberg(self):
720        """
721        Compare pinhole resolution smearing with romberg integration result.
722        """
723        pars = TEST_PARS_PINHOLE_SPHERE
724        data_string = TEST_DATA_PINHOLE_SPHERE
725        pars['radius'] *= 5
726        radius = pars['radius']
727
728        data = np.loadtxt(data_string.split('\n')).T
729        q, q_width, answer = data
730        answer = romberg_pinhole_1d(q, q_width, self.model, pars)
731        ## Getting 0.1% requires 5 sigma and 200 points per fringe
732        #q_calc = interpolate(pinhole_extend_q(q, q_width, nsigma=5),
733        #                     2*np.pi/radius/200)
734        #tol = 0.001
735        ## The default 3 sigma and no extra points gets 1%
736        q_calc, tol = None, 0.01
737        resolution = Pinhole1D(q, q_width, q_calc=q_calc)
738        output = self.Iq_sphere(pars, resolution)
739        if 0: # debug plot
740            import matplotlib.pyplot as plt
741            resolution = Perfect1D(q)
742            source = self.Iq_sphere(pars, resolution)
743            plt.loglog(q, source, '.')
744            plt.loglog(q, answer, '-', hold=True)
745            plt.loglog(q, output, '-', hold=True)
746            plt.show()
747        self.compare(q, output, answer, tol)
748
749    def test_slit(self):
750        """
751        Compare slit resolution smearing with NIST Igor SANS
752        """
753        pars = TEST_PARS_SLIT_SPHERE
754        data_string = TEST_DATA_SLIT_SPHERE
755
756        data = np.loadtxt(data_string.split('\n')).T
757        q, delta_qv, _, answer = data
758        resolution = Slit1D(q, width=delta_qv, height=0)
759        output = self.Iq_sphere(pars, resolution)
760        # TODO: eliminate Igor test since it is too inaccurate to be useful.
761        # This means we can eliminate the test data as well, and instead
762        # use a generated q vector.
763        self.compare(q, output, answer, 0.5)
764
765    def test_slit_romberg(self):
766        """
767        Compare slit resolution smearing with romberg integration result.
768        """
769        pars = TEST_PARS_SLIT_SPHERE
770        data_string = TEST_DATA_SLIT_SPHERE
771        radius = pars['radius']
772
773        data = np.loadtxt(data_string.split('\n')).T
774        q, delta_qv, _, answer = data
775        answer = romberg_slit_1d(q, delta_qv, 0., self.model, pars)
776        q_calc = slit_extend_q(interpolate(q, 2*np.pi/radius/20),
777                               delta_qv[0], 0.)
778        resolution = Slit1D(q, width=delta_qv, height=0, q_calc=q_calc)
779        output = self.Iq_sphere(pars, resolution)
780        # TODO: relative error should be lower
781        self.compare(q, output, answer, 0.025)
782
783    def test_ellipsoid(self):
784        """
785        Compare romberg integration for ellipsoid model.
786        """
787        from .core import load_model
788        pars = {
789            'scale':0.05,
790            'rpolar':500, 'requatorial':15000,
791            'sld':6, 'solvent_sld': 1,
792            }
793        form = load_model('ellipsoid', dtype='double')
794        q = np.logspace(log10(4e-5), log10(2.5e-2), 68)
795        width, height = 0.117, 0.
796        resolution = Slit1D(q, width=width, height=height)
797        answer = romberg_slit_1d(q, width, height, form, pars)
798        output = resolution.apply(eval_form(resolution.q_calc, form, pars))
799        # TODO: 10% is too much error; use better algorithm
800        #print(np.max(abs(answer-output)/answer))
801        self.compare(q, output, answer, 0.1)
802
803    #TODO: can sas q spacing be too sparse for the resolution calculation?
804    @unittest.skip("suppress sparse data test; not supported by current code")
805    def test_pinhole_sparse(self):
806        """
807        Compare pinhole resolution smearing with NIST Igor SANS on sparse data
808        """
809        pars = TEST_PARS_PINHOLE_SPHERE
810        data_string = TEST_DATA_PINHOLE_SPHERE
811
812        data = np.loadtxt(data_string.split('\n')).T
813        q, q_width, answer = data[:, ::20] # Take every nth point
814        resolution = Pinhole1D(q, q_width)
815        output = self.Iq_sphere(pars, resolution)
816        self.compare(q, output, answer, 1e-6)
817
818
819# pinhole sphere parameters
820TEST_PARS_PINHOLE_SPHERE = {
821    'scale': 1.0, 'background': 0.01,
822    'radius': 60.0, 'sld': 1, 'solvent_sld': 6.3,
823    }
824# Q, dQ, I(Q) calculated by NIST Igor SANS package
825TEST_DATA_PINHOLE_SPHERE = """\
8260.001278 0.0002847 2538.41176383
8270.001562 0.0002905 2536.91820405
8280.001846 0.0002956 2535.13182479
8290.002130 0.0003017 2533.06217813
8300.002414 0.0003087 2530.70378586
8310.002698 0.0003165 2528.05024192
8320.002982 0.0003249 2525.10408349
8330.003266 0.0003340 2521.86667499
8340.003550 0.0003437 2518.33907750
8350.003834 0.0003539 2514.52246995
8360.004118 0.0003646 2510.41798319
8370.004402 0.0003757 2506.02690988
8380.004686 0.0003872 2501.35067884
8390.004970 0.0003990 2496.38678318
8400.005253 0.0004112 2491.16237596
8410.005537 0.0004237 2485.63911673
8420.005821 0.0004365 2479.83657083
8430.006105 0.0004495 2473.75676948
8440.006389 0.0004628 2467.40145990
8450.006673 0.0004762 2460.77293372
8460.006957 0.0004899 2453.86724627
8470.007241 0.0005037 2446.69623838
8480.007525 0.0005177 2439.25775219
8490.007809 0.0005318 2431.55421398
8500.008093 0.0005461 2423.58785521
8510.008377 0.0005605 2415.36158137
8520.008661 0.0005750 2406.87009473
8530.008945 0.0005896 2398.12841186
8540.009229 0.0006044 2389.13360806
8550.009513 0.0006192 2379.88958042
8560.009797 0.0006341 2370.39776774
8570.010080 0.0006491 2360.69528793
8580.010360 0.0006641 2350.85169027
8590.010650 0.0006793 2340.42023633
8600.010930 0.0006945 2330.11206013
8610.011220 0.0007097 2319.20109972
8620.011500 0.0007251 2308.43503981
8630.011780 0.0007404 2297.44820179
8640.012070 0.0007558 2285.83853677
8650.012350 0.0007713 2274.41290746
8660.012640 0.0007868 2262.36219581
8670.012920 0.0008024 2250.51169731
8680.013200 0.0008180 2238.45596231
8690.013490 0.0008336 2225.76495666
8700.013770 0.0008493 2213.29618391
8710.014060 0.0008650 2200.19110751
8720.014340 0.0008807 2187.34050325
8730.014620 0.0008965 2174.30529864
8740.014910 0.0009123 2160.61632548
8750.015190 0.0009281 2147.21038112
8760.015470 0.0009440 2133.62023580
8770.015760 0.0009598 2119.37907426
8780.016040 0.0009757 2105.45234903
8790.016330 0.0009916 2090.86319102
8800.016610 0.0010080 2076.60576032
8810.016890 0.0010240 2062.19214565
8820.017180 0.0010390 2047.10550219
8830.017460 0.0010550 2032.38715621
8840.017740 0.0010710 2017.52560123
8850.018030 0.0010880 2001.99124318
8860.018310 0.0011040 1986.84662060
8870.018600 0.0011200 1971.03389745
8880.018880 0.0011360 1955.61395119
8890.019160 0.0011520 1940.08291563
8900.019450 0.0011680 1923.87672225
8910.019730 0.0011840 1908.10656374
8920.020020 0.0012000 1891.66297192
8930.020300 0.0012160 1875.66789021
8940.020580 0.0012320 1859.56357196
8950.020870 0.0012490 1842.79468290
8960.021150 0.0012650 1826.50064489
8970.021430 0.0012810 1810.11533702
8980.021720 0.0012970 1793.06840882
8990.022000 0.0013130 1776.51153580
9000.022280 0.0013290 1759.87201249
9010.022570 0.0013460 1742.57354412
9020.022850 0.0013620 1725.79397319
9030.023140 0.0013780 1708.35831550
9040.023420 0.0013940 1691.45256069
9050.023700 0.0014110 1674.48561783
9060.023990 0.0014270 1656.86525366
9070.024270 0.0014430 1639.79847285
9080.024550 0.0014590 1622.68887088
9090.024840 0.0014760 1604.96421100
9100.025120 0.0014920 1587.85768129
9110.025410 0.0015080 1569.99297335
9120.025690 0.0015240 1552.84580279
9130.025970 0.0015410 1535.54074115
9140.026260 0.0015570 1517.75249337
9150.026540 0.0015730 1500.40115023
9160.026820 0.0015900 1483.03632237
9170.027110 0.0016060 1465.05942429
9180.027390 0.0016220 1447.67682181
9190.027670 0.0016390 1430.46495191
9200.027960 0.0016550 1412.49232282
9210.028240 0.0016710 1395.13182318
9220.028520 0.0016880 1377.93439837
9230.028810 0.0017040 1359.99528971
9240.029090 0.0017200 1342.67274512
9250.029370 0.0017370 1325.55375609
926"""
927
928# Slit sphere parameters
929TEST_PARS_SLIT_SPHERE = {
930    'scale': 0.01, 'background': 0.01,
931    'radius': 60000, 'sld': 1, 'solvent_sld': 4,
932    }
933# Q dQ I(Q) I_smeared(Q)
934TEST_DATA_SLIT_SPHERE = """\
9352.26097e-05 0.117 5.5781372896e+09 1.4626077708e+06
9362.53847e-05 0.117 5.0363141458e+09 1.3117318023e+06
9372.81597e-05 0.117 4.4850108103e+09 1.1594863713e+06
9383.09347e-05 0.117 3.9364658459e+09 1.0094881630e+06
9393.37097e-05 0.117 3.4019975074e+09 8.6518941303e+05
9403.92597e-05 0.117 2.4139519814e+09 6.0232158311e+05
9414.48097e-05 0.117 1.5816877820e+09 3.8739994090e+05
9425.03597e-05 0.117 9.3715407224e+08 2.2745304775e+05
9435.59097e-05 0.117 4.8387917428e+08 1.2101295768e+05
9446.14597e-05 0.117 2.0193586928e+08 6.0055107771e+04
9456.70097e-05 0.117 5.5886110911e+07 3.2749521065e+04
9467.25597e-05 0.117 3.7782348010e+06 2.6350963616e+04
9477.81097e-05 0.117 5.3407817904e+06 2.9624963314e+04
9488.36597e-05 0.117 2.7975485523e+07 3.4403962254e+04
9498.92097e-05 0.117 4.9845448282e+07 3.6130017903e+04
9509.47597e-05 0.117 6.0092588905e+07 3.3495107849e+04
9511.00310e-04 0.117 5.6823430831e+07 2.7475726279e+04
9521.05860e-04 0.117 4.3857024036e+07 2.0144282226e+04
9531.11410e-04 0.117 2.7277144760e+07 1.3647403260e+04
9541.22510e-04 0.117 3.3119334113e+06 6.6519711526e+03
9551.33610e-04 0.117 1.4412859402e+06 6.9726212813e+03
9561.44710e-04 0.117 8.5620162463e+06 8.1441335775e+03
9571.55810e-04 0.117 9.6957429033e+06 6.4559996521e+03
9581.66910e-04 0.117 4.3818341914e+06 3.6252154156e+03
9591.78010e-04 0.117 2.7448997387e+05 2.4006505342e+03
9601.89110e-04 0.117 8.0472009936e+05 2.8187789089e+03
9612.00210e-04 0.117 2.8149552834e+06 3.0915662855e+03
9622.11310e-04 0.117 2.7510907861e+06 2.3722530293e+03
9632.22410e-04 0.117 1.0053133293e+06 1.4473468311e+03
9642.33510e-04 0.117 5.8428305052e+03 1.2048540556e+03
9652.44610e-04 0.117 5.1699305004e+05 1.4625670042e+03
9662.55710e-04 0.117 1.2120227268e+06 1.5010705973e+03
9672.66810e-04 0.117 9.7896842846e+05 1.1336343426e+03
9682.77910e-04 0.117 2.5507264791e+05 8.1848818080e+02
9693.05660e-04 0.117 5.2403101181e+05 7.4913374357e+02
9703.33410e-04 0.117 5.8699343809e+04 4.4669964560e+02
9713.61160e-04 0.117 3.0844327150e+05 4.6774007542e+02
9723.88910e-04 0.117 8.3360142970e+03 2.7169550220e+02
9734.16660e-04 0.117 1.8630080583e+05 3.0710983679e+02
9744.44410e-04 0.117 3.1616804732e-01 1.7959006831e+02
9754.72160e-04 0.117 1.1299016314e+05 2.0763952339e+02
9764.99910e-04 0.117 2.9952522747e+03 1.2536542765e+02
9775.27660e-04 0.117 6.7625695649e+04 1.4013969777e+02
9785.55410e-04 0.117 7.6927460089e+03 8.2145593180e+01
9796.10910e-04 0.117 1.1229057779e+04 8.4519745643e+01
9806.66410e-04 0.117 1.3035567943e+04 8.1554625609e+01
9817.21910e-04 0.117 1.3309931343e+04 7.4437319172e+01
9827.77410e-04 0.117 1.2462626212e+04 6.4697088261e+01
9838.32910e-04 0.117 1.0912927143e+04 5.3773301044e+01
9848.88410e-04 0.117 9.0172597469e+03 4.2843375753e+01
9859.43910e-04 0.117 7.0496495917e+03 3.2771032724e+01
9869.99410e-04 0.117 5.2030483682e+03 2.4113557144e+01
9871.05491e-03 0.117 3.5988976711e+03 1.7160773658e+01
9881.11041e-03 0.117 2.2996060652e+03 1.2016626459e+01
9891.22141e-03 0.117 6.4766590598e+02 6.0373017740e+00
9901.33241e-03 0.117 4.1963483264e+01 4.5215452974e+00
9911.44341e-03 0.117 6.3370708246e+01 5.1054681903e+00
9921.55441e-03 0.117 3.0736750577e+02 5.9176165298e+00
9931.66541e-03 0.117 5.0327682399e+02 5.9815000189e+00
9941.77641e-03 0.117 5.4084331454e+02 5.1634639625e+00
9951.88741e-03 0.117 4.3488671756e+02 3.8535158148e+00
9961.99841e-03 0.117 2.6322287860e+02 2.5824997753e+00
9972.10941e-03 0.117 1.0793633150e+02 1.7315517194e+00
9982.22041e-03 0.117 1.8474448850e+01 1.4077213604e+00
9992.33141e-03 0.117 1.5864062279e+00 1.4771560682e+00
10002.44241e-03 0.117 3.2267213848e+01 1.6916253448e+00
10012.55341e-03 0.117 7.4289116207e+01 1.8274751193e+00
10022.66441e-03 0.117 9.9000521929e+01 1.7706812289e+00
1003"""
1004
1005def main():
1006    """
1007    Run tests given is sys.argv.
1008
1009    Returns 0 if success or 1 if any tests fail.
1010    """
1011    import sys
1012    import xmlrunner
1013
1014    suite = unittest.TestSuite()
1015    suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]))
1016
1017    runner = xmlrunner.XMLTestRunner(output='logs')
1018    result = runner.run(suite)
1019    return 1 if result.failures or result.errors else 0
1020
1021
1022############################################################################
1023# usage demo
1024############################################################################
1025
1026def _eval_demo_1d(resolution, title):
1027    import sys
1028    from sasmodels import core
1029    name = sys.argv[1] if len(sys.argv) > 1 else 'cylinder'
1030
1031    if name == 'cylinder':
1032        pars = {'length':210, 'radius':500}
1033    elif name == 'teubner_strey':
1034        pars = {'a2':0.003, 'c1':-1e4, 'c2':1e10, 'background':0.312643}
1035    elif name == 'sphere' or name == 'spherepy':
1036        pars = TEST_PARS_SLIT_SPHERE
1037    elif name == 'ellipsoid':
1038        pars = {
1039            'scale':0.05,
1040            'rpolar':500, 'requatorial':15000,
1041            'sld':6, 'solvent_sld': 1,
1042            }
1043    else:
1044        pars = {}
1045    defn = core.load_model_definition(name)
1046    model = core.load_model(defn)
1047
1048    kernel = core.make_kernel(model, [resolution.q_calc])
1049    theory = core.call_kernel(kernel, pars)
1050    Iq = resolution.apply(theory)
1051
1052    if isinstance(resolution, Slit1D):
1053        width, height = resolution.width, resolution.height
1054        Iq_romb = romberg_slit_1d(resolution.q, width, height, model, pars)
1055    else:
1056        dq = resolution.q_width
1057        Iq_romb = romberg_pinhole_1d(resolution.q, dq, model, pars)
1058
1059    import matplotlib.pyplot as plt
1060    plt.loglog(resolution.q_calc, theory, label='unsmeared')
1061    plt.loglog(resolution.q, Iq, label='smeared', hold=True)
1062    plt.loglog(resolution.q, Iq_romb, label='romberg smeared', hold=True)
1063    plt.legend()
1064    plt.title(title)
1065    plt.xlabel("Q (1/Ang)")
1066    plt.ylabel("I(Q) (1/cm)")
1067
1068def demo_pinhole_1d():
1069    q = np.logspace(-4, np.log10(0.2), 400)
1070    q_width = 0.1*q
1071    resolution = Pinhole1D(q, q_width)
1072    _eval_demo_1d(resolution, title="10% dQ/Q Pinhole Resolution")
1073
1074def demo_slit_1d():
1075    q = np.logspace(-4, np.log10(0.2), 100)
1076    w = h = 0.
1077    #w = 0.000000277790
1078    w = 0.0277790
1079    #h = 0.00277790
1080    #h = 0.0277790
1081    resolution = Slit1D(q, w, h)
1082    _eval_demo_1d(resolution, title="(%g,%g) Slit Resolution"%(w, h))
1083
1084def demo():
1085    import matplotlib.pyplot as plt
1086    plt.subplot(121)
1087    demo_pinhole_1d()
1088    #plt.yscale('linear')
1089    plt.subplot(122)
1090    demo_slit_1d()
1091    #plt.yscale('linear')
1092    plt.show()
1093
1094
1095if __name__ == "__main__":
1096    #demo()
1097    main()
Note: See TracBrowser for help on using the repository browser.