source: sasmodels/sasmodels/resolution.py @ 0444c02

core_shell_microgelscostrafo411magnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 0444c02 was 0444c02, checked in by jhbakker, 8 years ago

Changes for SESANS integration, next is merge with ajj_sesans

  • Property mode set to 100644
File size: 39.2 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
8from scipy.special import erf  # type: ignore
9from numpy import sqrt, log, log10, exp, pi  # type: ignore
10import numpy as np  # type: ignore
11
12__all__ = ["Resolution", "Perfect1D", "Pinhole1D", "Slit1D",
13           "apply_resolution_matrix", "pinhole_resolution", "slit_resolution",
14           "pinhole_extend_q", "slit_extend_q", "bin_edges",
15           "interpolate", "linear_extrapolation", "geometric_extrapolation",
16          ]
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  # type: np.ndarray
38    q_calc = None  # type: np.ndarray
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
45class Perfect1D(Resolution):
46    """
47    Resolution function to use when there is no actual resolution smearing
48    to be applied.  It has the same interface as the other resolution
49    functions, but returns the identity function.
50    """
51    def __init__(self, q):
52        self.q_calc = self.q = q
53
54    def apply(self, theory):
55        return theory
56
57
58class Pinhole1D(Resolution):
59    r"""
60    Pinhole aperture with q-dependent gaussian resolution.
61
62    *q* points at which the data is measured.
63
64    *q_width* gaussian 1-sigma resolution at each data point.
65
66    *q_calc* is the list of points to calculate, or None if this should
67    be estimated from the *q* and *q_width*.
68    """
69    def __init__(self, q, q_width, q_calc=None, nsigma=3):
70        #*min_step* is the minimum point spacing to use when computing the
71        #underlying model.  It should be on the order of
72        #$\tfrac{1}{10}\tfrac{2\pi}{d_\text{max}}$ to make sure that fringes
73        #are computed with sufficient density to avoid aliasing effects.
74
75        # Protect against calls with q_width=0.  The extend_q function will
76        # not extend the q if q_width is 0, but q_width must be non-zero when
77        # constructing the weight matrix to avoid division by zero errors.
78        # In practice this should never be needed, since resolution should
79        # default to Perfect1D if the pinhole geometry is not defined.
80        self.q, self.q_width = q, q_width
81        self.q_calc = (pinhole_extend_q(q, q_width, nsigma=nsigma)
82                       if q_calc is None else np.sort(q_calc))
83        self.weight_matrix = pinhole_resolution(self.q_calc, self.q,
84                                np.maximum(q_width, MINIMUM_RESOLUTION))
85
86    def apply(self, theory):
87        return apply_resolution_matrix(self.weight_matrix, theory)
88
89
90class Slit1D(Resolution):
91    """
92    Slit aperture with resolution function.
93
94    *q* points at which the data is measured.
95
96    *dqx* slit width in qx
97
98    *dqy* slit height in qy
99
100    *q_calc* is the list of points to calculate, or None if this should
101    be estimated from the *q* and *q_width*.
102
103    The *weight_matrix* is computed by :func:`slit1d_resolution`
104    """
105    def __init__(self, q, qx_width, qy_width=0., q_calc=None):
106        # Remember what width/dqy was used even though we won't need them
107        # after the weight matrix is constructed
108        self.qx_width, self.qy_width = qx_width, qy_width
109
110        # Allow independent resolution on each point even though it is not
111        # needed in practice.
112        if np.isscalar(qx_width):
113            qx_width = np.ones(len(q))*qx_width
114        else:
115            qx_width = np.asarray(qx_width)
116        if np.isscalar(qy_width):
117            qy_width = np.ones(len(q))*qy_width
118        else:
119            qy_width = np.asarray(qy_width)
120
121        self.q = q.flatten()
122        self.q_calc = slit_extend_q(q, qx_width, qy_width) \
123            if q_calc is None else np.sort(q_calc)
124        self.weight_matrix = \
125            slit_resolution(self.q_calc, self.q, qx_width, qy_width)
126
127    def apply(self, theory):
128        return apply_resolution_matrix(self.weight_matrix, theory)
129
130
131def apply_resolution_matrix(weight_matrix, theory):
132    """
133    Apply the resolution weight matrix to the computed theory function.
134    """
135    #print("apply shapes", theory.shape, weight_matrix.shape)
136    Iq = np.dot(theory[None, :], weight_matrix)
137    #print("result shape",Iq.shape)
138    return Iq.flatten()
139
140
141def pinhole_resolution(q_calc, q, q_width):
142    """
143    Compute the convolution matrix *W* for pinhole resolution 1-D data.
144
145    Each row *W[i]* determines the normalized weight that the corresponding
146    points *q_calc* contribute to the resolution smeared point *q[i]*.  Given
147    *W*, the resolution smearing can be computed using *dot(W,q)*.
148
149    *q_calc* must be increasing.  *q_width* must be greater than zero.
150    """
151    # The current algorithm is a midpoint rectangle rule.  In the test case,
152    # neither trapezoid nor Simpson's rule improved the accuracy.
153    edges = bin_edges(q_calc)
154    edges[edges < 0.0] = 0.0 # clip edges below zero
155    cdf = erf((edges[:, None] - q[None, :]) / (sqrt(2.0)*q_width)[None, :])
156    weights = cdf[1:] - cdf[:-1]
157    weights /= np.sum(weights, axis=0)[None, :]
158    return weights
159
160
161def slit_resolution(q_calc, q, width, height, n_height=30):
162    r"""
163    Build a weight matrix to compute *I_s(q)* from *I(q_calc)*, given
164    $q_\perp$ = *width* and $q_\parallel$ = *height*.  *n_height* is
165    is the number of steps to use in the integration over $q_\parallel$
166    when both $q_\perp$ and $q_\parallel$ are non-zero.
167
168    Each $q$ can have an independent width and height value even though
169    current instruments use the same slit setting for all measured points.
170
171    If slit height is large relative to width, use:
172
173    .. math::
174
175        I_s(q_i) = \frac{1}{\Delta q_\perp}
176            \int_0^{\Delta q_\perp}
177                I\left(\sqrt{q_i^2 + q_\perp^2}\right) \,dq_\perp
178
179    If slit width is large relative to height, use:
180
181    .. math::
182
183        I_s(q_i) = \frac{1}{2 \Delta q_\parallel}
184            \int_{-\Delta q_\parallel}^{\Delta q_\parallel}
185                I\left(|q_i + q_\parallel|\right) \,dq_\parallel
186
187    For a mixture of slit width and height use:
188
189    .. math::
190
191        I_s(q_i) = \frac{1}{2 \Delta q_\parallel \Delta q_\perp}
192            \int_{-\Delta q_\parallel}^{\Delta q_\parallel}
193            \int_0^{\Delta q_\perp}
194                I\left(\sqrt{(q_i + q_\parallel)^2 + q_\perp^2}\right)
195                \,dq_\perp dq_\parallel
196
197    **Definition**
198
199    We are using the mid-point integration rule to assign weights to each
200    element of a weight matrix $W$ so that
201
202    .. math::
203
204        I_s(q) = W\,I(q_\text{calc})
205
206    If *q_calc* is at the mid-point, we can infer the bin edges from the
207    pairwise averages of *q_calc*, adding the missing edges before
208    *q_calc[0]* and after *q_calc[-1]*.
209
210    For $q_\parallel = 0$, the smeared value can be computed numerically
211    using the $u$ substitution
212
213    .. math::
214
215        u_j = \sqrt{q_j^2 - q^2}
216
217    This gives
218
219    .. math::
220
221        I_s(q) \approx \sum_j I(u_j) \Delta u_j
222
223    where $I(u_j)$ is the value at the mid-point, and $\Delta u_j$ is the
224    difference between consecutive edges which have been first converted
225    to $u$.  Only $u_j \in [0, \Delta q_\perp]$ are used, which corresponds
226    to $q_j \in \left[q, \sqrt{q^2 + \Delta q_\perp}\right]$, so
227
228    .. math::
229
230        W_{ij} = \frac{1}{\Delta q_\perp} \Delta u_j
231               = \frac{1}{\Delta q_\perp} \left(
232                    \sqrt{q_{j+1}^2 - q_i^2} - \sqrt{q_j^2 - q_i^2} \right)
233            \ \text{if}\  q_j \in \left[q_i, \sqrt{q_i^2 + q_\perp^2}\right]
234
235    where $I_s(q_i)$ is the theory function being computed and $q_j$ are the
236    mid-points between the calculated values in *q_calc*.  We tweak the
237    edges of the initial and final intervals so that they lie on integration
238    limits.
239
240    (To be precise, the transformed midpoint $u(q_j)$ is not necessarily the
241    midpoint of the edges $u((q_{j-1}+q_j)/2)$ and $u((q_j + q_{j+1})/2)$,
242    but it is at least in the interval, so the approximation is going to be
243    a little better than the left or right Riemann sum, and should be
244    good enough for our purposes.)
245
246    For $q_\perp = 0$, the $u$ substitution is simpler:
247
248    .. math::
249
250        u_j = \left|q_j - q\right|
251
252    so
253
254    .. math::
255
256        W_{ij} = \frac{1}{2 \Delta q_\parallel} \Delta u_j
257            = \frac{1}{2 \Delta q_\parallel} (q_{j+1} - q_j)
258            \ \text{if}\ q_j \in
259                \left[q-\Delta q_\parallel, q+\Delta q_\parallel\right]
260
261    However, we need to support cases were $u_j < 0$, which means using
262    $2 (q_{j+1} - q_j)$ when $q_j \in \left[0, q_\parallel-q_i\right]$.
263    This is not an issue for $q_i > q_\parallel$.
264
265    For both $q_\perp > 0$ and $q_\parallel > 0$ we perform a 2 dimensional
266    integration with
267
268    .. math::
269
270        u_{jk} = \sqrt{q_j^2 - (q + (k\Delta q_\parallel/L))^2}
271            \ \text{for}\ k = -L \ldots L
272
273    for $L$ = *n_height*.  This gives
274
275    .. math::
276
277        W_{ij} = \frac{1}{2 \Delta q_\perp q_\parallel}
278            \sum_{k=-L}^L \Delta u_{jk}
279                \left(\frac{\Delta q_\parallel}{2 L + 1}\right)
280
281
282    """
283    #np.set_printoptions(precision=6, linewidth=10000)
284
285    # The current algorithm is a midpoint rectangle rule.
286    q_edges = bin_edges(q_calc) # Note: requires q > 0
287    q_edges[q_edges < 0.0] = 0.0 # clip edges below zero
288    weights = np.zeros((len(q), len(q_calc)), 'd')
289
290    #print(q_calc)
291    for i, (qi, w, h) in enumerate(zip(q, width, height)):
292        if w == 0. and h == 0.:
293            # Perfect resolution, so return the theory value directly.
294            # Note: assumes that q is a subset of q_calc.  If qi need not be
295            # in q_calc, then we can do a weighted interpolation by looking
296            # up qi in q_calc, then weighting the result by the relative
297            # distance to the neighbouring points.
298            weights[i, :] = (q_calc == qi)
299        elif h == 0:
300            weights[i, :] = _q_perp_weights(q_edges, qi, w)
301        elif w == 0:
302            in_x = 1.0 * ((q_calc >= qi-h) & (q_calc <= qi+h))
303            abs_x = 1.0*(q_calc < abs(qi - h)) if qi < h else 0.
304            #print(qi - h, qi + h)
305            #print(in_x + abs_x)
306            weights[i, :] = (in_x + abs_x) * np.diff(q_edges) / (2*h)
307        else:
308            for k in range(-n_height, h_height+1):
309                weights[i, :] += _q_perp_weights(q_edges, qi+k*h/n_height, w)
310            weights[i, :] /= 2*n_height + 1
311
312    return weights.T
313
314
315def _q_perp_weights(q_edges, qi, w):
316    # Convert bin edges from q to u
317    u_limit = np.sqrt(qi**2 + w**2)
318    u_edges = q_edges**2 - qi**2
319    u_edges[q_edges < abs(qi)] = 0.
320    u_edges[q_edges > u_limit] = u_limit**2 - qi**2
321    weights = np.diff(np.sqrt(u_edges))/w
322    #print("i, qi",i,qi,qi+width)
323    #print(q_calc)
324    #print(weights)
325    return weights
326
327
328def pinhole_extend_q(q, q_width, nsigma=3):
329    """
330    Given *q* and *q_width*, find a set of sampling points *q_calc* so
331    that each point $I(q)$ has sufficient support from the underlying
332    function.
333    """
334    q_min, q_max = np.min(q - nsigma*q_width), np.max(q + nsigma*q_width)
335    return linear_extrapolation(q, q_min, q_max)
336
337
338def slit_extend_q(q, width, height):
339    """
340    Given *q*, *width* and *height*, find a set of sampling points *q_calc* so
341    that each point I(q) has sufficient support from the underlying
342    function.
343    """
344    q_min, q_max = np.min(q-height), np.max(np.sqrt((q+height)**2 + width**2))
345
346    return geometric_extrapolation(q, q_min, q_max)
347
348
349def bin_edges(x):
350    """
351    Determine bin edges from bin centers, assuming that edges are centered
352    between the bins.
353
354    Note: this uses the arithmetic mean, which may not be appropriate for
355    log-scaled data.
356    """
357    if len(x) < 2 or (np.diff(x) < 0).any():
358        raise ValueError("Expected bins to be an increasing set")
359    edges = np.hstack([
360        x[0]  - 0.5*(x[1]  - x[0]),  # first point minus half first interval
361        0.5*(x[1:] + x[:-1]),        # mid points of all central intervals
362        x[-1] + 0.5*(x[-1] - x[-2]), # last point plus half last interval
363        ])
364    return edges
365
366
367def interpolate(q, max_step):
368    """
369    Returns *q_calc* with points spaced at most max_step apart.
370    """
371    step = np.diff(q)
372    index = step > max_step
373    if np.any(index):
374        inserts = []
375        for q_i, step_i in zip(q[:-1][index], step[index]):
376            n = np.ceil(step_i/max_step)
377            inserts.extend(q_i + np.arange(1, n)*(step_i/n))
378        # Extend a couple of fringes beyond the end of the data
379        inserts.extend(q[-1] + np.arange(1, 8)*max_step)
380        q_calc = np.sort(np.hstack((q, inserts)))
381    else:
382        q_calc = q
383    return q_calc
384
385
386def linear_extrapolation(q, q_min, q_max):
387    """
388    Extrapolate *q* out to [*q_min*, *q_max*] using the step size in *q* as
389    a guide.  Extrapolation below uses about the same size as the first
390    interval.  Extrapolation above uses about the same size as the final
391    interval.
392
393    if *q_min* is zero or less then *q[0]/10* is used instead.
394    """
395    q = np.sort(q)
396    if q_min + 2*MINIMUM_RESOLUTION < q[0]:
397        if q_min <= 0: q_min = q_min*MIN_Q_SCALE_FOR_NEGATIVE_Q_EXTRAPOLATION
398        n_low = np.ceil((q[0]-q_min) / (q[1]-q[0])) if q[1] > q[0] else 15
399        q_low = np.linspace(q_min, q[0], n_low+1)[:-1]
400    else:
401        q_low = []
402    if q_max - 2*MINIMUM_RESOLUTION > q[-1]:
403        n_high = np.ceil((q_max-q[-1]) / (q[-1]-q[-2])) if q[-1] > q[-2] else 15
404        q_high = np.linspace(q[-1], q_max, n_high+1)[1:]
405    else:
406        q_high = []
407    return np.concatenate([q_low, q, q_high])
408
409
410def geometric_extrapolation(q, q_min, q_max, points_per_decade=None):
411    r"""
412    Extrapolate *q* to [*q_min*, *q_max*] using geometric steps, with the
413    average geometric step size in *q* as the step size.
414
415    if *q_min* is zero or less then *q[0]/10* is used instead.
416
417    *points_per_decade* sets the ratio between consecutive steps such
418    that there will be $n$ points used for every factor of 10 increase
419    in *q*.
420
421    If *points_per_decade* is not given, it will be estimated as follows.
422    Starting at $q_1$ and stepping geometrically by $\Delta q$ to $q_n$
423    in $n$ points gives a geometric average of:
424
425    .. math::
426
427         \log \Delta q = (\log q_n - log q_1) / (n - 1)
428
429    From this we can compute the number of steps required to extend $q$
430    from $q_n$ to $q_\text{max}$ by $\Delta q$ as:
431
432    .. math::
433
434         n_\text{extend} = (\log q_\text{max} - \log q_n) / \log \Delta q
435
436    Substituting:
437
438    .. math::
439
440         n_\text{extend} = (n-1) (\log q_\text{max} - \log q_n)
441            / (\log q_n - log q_1)
442    """
443    q = np.sort(q)
444    if points_per_decade is None:
445        log_delta_q = (len(q) - 1) / (log(q[-1]) - log(q[0]))
446    else:
447        log_delta_q = log(10.) / points_per_decade
448    if q_min < q[0]:
449        if q_min < 0: q_min = q[0]*MIN_Q_SCALE_FOR_NEGATIVE_Q_EXTRAPOLATION
450        n_low = log_delta_q * (log(q[0])-log(q_min))
451        q_low = np.logspace(log10(q_min), log10(q[0]), np.ceil(n_low)+1)[:-1]
452    else:
453        q_low = []
454    if q_max > q[-1]:
455        n_high = log_delta_q * (log(q_max)-log(q[-1]))
456        q_high = np.logspace(log10(q[-1]), log10(q_max), np.ceil(n_high)+1)[1:]
457    else:
458        q_high = []
459    return np.concatenate([q_low, q, q_high])
460
461
462############################################################################
463# unit tests
464############################################################################
465import unittest
466
467
468def eval_form(q, form, pars):
469    """
470    Return the SAS model evaluated at *q*.
471
472    *form* is the SAS model returned from :fun:`core.load_model`.
473
474    *pars* are the parameter values to use when evaluating.
475    """
476    from sasmodels import direct_model
477    kernel = form.make_kernel([q])
478    theory = direct_model.call_kernel(kernel, pars)
479    kernel.release()
480    return theory
481
482
483def gaussian(q, q0, dq):
484    """
485    Return the Gaussian resolution function.
486
487    *q0* is the center, *dq* is the width and *q* are the points to evaluate.
488    """
489    return exp(-0.5*((q-q0)/dq)**2)/(sqrt(2*pi)*dq)
490
491
492def romberg_slit_1d(q, width, height, form, pars):
493    """
494    Romberg integration for slit resolution.
495
496    This is an adaptive integration technique.  It is called with settings
497    that make it slow to evaluate but give it good accuracy.
498    """
499    from scipy.integrate import romberg  # type: ignore
500
501    par_set = set([p.name for p in form.info.parameters.call_parameters])
502    if any(k not in par_set for k in pars.keys()):
503        extra = set(pars.keys()) - par_set
504        raise ValueError("bad parameters: [%s] not in [%s]"
505                         % (", ".join(sorted(extra)),
506                            ", ".join(sorted(pars.keys()))))
507
508    if np.isscalar(width):
509        width = [width]*len(q)
510    if np.isscalar(height):
511        height = [height]*len(q)
512    _int_w = lambda w, qi: eval_form(sqrt(qi**2 + w**2), form, pars)
513    _int_h = lambda h, qi: eval_form(abs(qi+h), form, pars)
514    # If both width and height are defined, then it is too slow to use dblquad.
515    # Instead use trapz on a fixed grid, interpolated into the I(Q) for
516    # the extended Q range.
517    #_int_wh = lambda w, h, qi: eval_form(sqrt((qi+h)**2 + w**2), form, pars)
518    q_calc = slit_extend_q(q, np.asarray(width), np.asarray(height))
519    Iq = eval_form(q_calc, form, pars)
520    result = np.empty(len(q))
521    for i, (qi, w, h) in enumerate(zip(q, width, height)):
522        if h == 0.:
523            total = romberg(_int_w, 0, w, args=(qi,),
524                            divmax=100, vec_func=True, tol=0, rtol=1e-8)
525            result[i] = total/w
526        elif w == 0.:
527            total = romberg(_int_h, -h, h, args=(qi,),
528                            divmax=100, vec_func=True, tol=0, rtol=1e-8)
529            result[i] = total/(2*h)
530        else:
531            w_grid = np.linspace(0, w, 21)[None, :]
532            h_grid = np.linspace(-h, h, 23)[:, None]
533            u_sub = sqrt((qi+h_grid)**2 + w_grid**2)
534            f_at_u = np.interp(u_sub, q_calc, Iq)
535            #print(np.trapz(Iu, w_grid, axis=1))
536            total  = np.trapz(np.trapz(f_at_u, w_grid, axis=1), h_grid[:, 0])
537            result[i] = total / (2*h*w)
538            # from scipy.integrate import dblquad
539            # r, err = dblquad(_int_wh, -h, h, lambda h: 0., lambda h: w,
540            #                  args=(qi,))
541            # result[i] = r/(w*2*h)
542
543    # r should be [float, ...], but it is [array([float]), array([float]),...]
544    return result
545
546
547def romberg_pinhole_1d(q, q_width, form, pars, nsigma=5):
548    """
549    Romberg integration for pinhole resolution.
550
551    This is an adaptive integration technique.  It is called with settings
552    that make it slow to evaluate but give it good accuracy.
553    """
554    from scipy.integrate import romberg  # type: ignore
555
556    par_set = set([p.name for p in form.info.parameters.call_parameters])
557    if any(k not in par_set for k in pars.keys()):
558        extra = set(pars.keys()) - par_set
559        raise ValueError("bad parameters: [%s] not in [%s]"
560                         % (", ".join(sorted(extra)),
561                            ", ".join(sorted(pars.keys()))))
562
563    func = lambda q, q0, dq: eval_form(q, form, pars)*gaussian(q, q0, dq)
564    total = [romberg(func, max(qi-nsigma*dqi, 1e-10*q[0]), qi+nsigma*dqi,
565                     args=(qi, dqi), divmax=100, vec_func=True,
566                     tol=0, rtol=1e-8)
567             for qi, dqi in zip(q, q_width)]
568    return np.asarray(total).flatten()
569
570
571class ResolutionTest(unittest.TestCase):
572    """
573    Test the resolution calculations.
574    """
575
576    def setUp(self):
577        self.x = 0.001*np.arange(1, 11)
578        self.y = self.Iq(self.x)
579
580    def Iq(self, q):
581        "Linear function for resolution unit test"
582        return 12.0 - 1000.0*q
583
584    def test_perfect(self):
585        """
586        Perfect resolution and no smearing.
587        """
588        resolution = Perfect1D(self.x)
589        theory = self.Iq(resolution.q_calc)
590        output = resolution.apply(theory)
591        np.testing.assert_equal(output, self.y)
592
593    def test_slit_zero(self):
594        """
595        Slit smearing with perfect resolution.
596        """
597        resolution = Slit1D(self.x, qx_width=0, qy_width=0, q_calc=self.x)
598        theory = self.Iq(resolution.q_calc)
599        output = resolution.apply(theory)
600        np.testing.assert_equal(output, self.y)
601
602    @unittest.skip("not yet supported")
603    def test_slit_high(self):
604        """
605        Slit smearing with height 0.005
606        """
607        resolution = Slit1D(self.x, qx_width=0, qy_width=0.005, q_calc=self.x)
608        theory = self.Iq(resolution.q_calc)
609        output = resolution.apply(theory)
610        answer = [
611            9.0618, 8.6402, 8.1187, 7.1392, 6.1528,
612            5.5555, 4.5584, 3.5606, 2.5623, 2.0000,
613            ]
614        np.testing.assert_allclose(output, answer, atol=1e-4)
615
616    @unittest.skip("not yet supported")
617    def test_slit_both_high(self):
618        """
619        Slit smearing with width < 100*height.
620        """
621        q = np.logspace(-4, -1, 10)
622        resolution = Slit1D(q, qx_width=0.2, qy_width=np.inf)
623        theory = 1000*self.Iq(resolution.q_calc**4)
624        output = resolution.apply(theory)
625        answer = [
626            8.85785, 8.43012, 7.92687, 6.94566, 6.03660,
627            5.40363, 4.40655, 3.40880, 2.41058, 2.00000,
628            ]
629        np.testing.assert_allclose(output, answer, atol=1e-4)
630
631    @unittest.skip("not yet supported")
632    def test_slit_wide(self):
633        """
634        Slit smearing with width 0.0002
635        """
636        resolution = Slit1D(self.x, qx_width=0.0002, qy_width=0, 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    @unittest.skip("not yet supported")
645    def test_slit_both_wide(self):
646        """
647        Slit smearing with width > 100*height.
648        """
649        resolution = Slit1D(self.x, qx_width=0.0002, qy_width=0.000001,
650                            q_calc=self.x)
651        theory = self.Iq(resolution.q_calc)
652        output = resolution.apply(theory)
653        answer = [
654            11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0,
655            ]
656        np.testing.assert_allclose(output, answer, atol=1e-4)
657
658    def test_pinhole_zero(self):
659        """
660        Pinhole smearing with perfect resolution
661        """
662        resolution = Pinhole1D(self.x, 0.0*self.x)
663        theory = self.Iq(resolution.q_calc)
664        output = resolution.apply(theory)
665        np.testing.assert_equal(output, self.y)
666
667    def test_pinhole(self):
668        """
669        Pinhole smearing with dQ = 0.001 [Note: not dQ/Q = 0.001]
670        """
671        resolution = Pinhole1D(self.x, 0.001*np.ones_like(self.x),
672                               q_calc=self.x)
673        theory = 12.0-1000.0*resolution.q_calc
674        output = resolution.apply(theory)
675        answer = [
676            10.44785079, 9.84991299, 8.98101708,
677            7.99906585, 6.99998311, 6.00001689,
678            5.00093415, 4.01898292, 3.15008701, 2.55214921,
679            ]
680        np.testing.assert_allclose(output, answer, atol=1e-8)
681
682
683class IgorComparisonTest(unittest.TestCase):
684    """
685    Test resolution calculations against those returned by Igor.
686    """
687
688    def setUp(self):
689        self.pars = TEST_PARS_PINHOLE_SPHERE
690        from sasmodels import core
691        self.model = core.load_model("sphere", dtype='double')
692
693    def _eval_sphere(self, pars, resolution):
694        from sasmodels import direct_model
695        kernel = self.model.make_kernel([resolution.q_calc])
696        theory = direct_model.call_kernel(kernel, pars)
697        result = resolution.apply(theory)
698        kernel.release()
699        return result
700
701    def _compare(self, q, output, answer, tolerance):
702        #err = (output - answer)/answer
703        #idx = abs(err) >= tolerance
704        #problem = zip(q[idx], output[idx], answer[idx], err[idx])
705        #print("\n".join(str(v) for v in problem))
706        np.testing.assert_allclose(output, answer, rtol=tolerance)
707
708    def test_perfect(self):
709        """
710        Compare sphere model with NIST Igor SANS, no resolution smearing.
711        """
712        pars = TEST_PARS_SLIT_SPHERE
713        data_string = TEST_DATA_SLIT_SPHERE
714
715        data = np.loadtxt(data_string.split('\n')).T
716        q, _, answer, _ = data
717        resolution = Perfect1D(q)
718        output = self._eval_sphere(pars, resolution)
719        self._compare(q, output, answer, 1e-6)
720
721    def test_pinhole(self):
722        """
723        Compare pinhole resolution smearing with NIST Igor SANS
724        """
725        pars = TEST_PARS_PINHOLE_SPHERE
726        data_string = TEST_DATA_PINHOLE_SPHERE
727
728        data = np.loadtxt(data_string.split('\n')).T
729        q, q_width, answer = data
730        resolution = Pinhole1D(q, q_width)
731        output = self._eval_sphere(pars, resolution)
732        # TODO: relative error should be lower
733        self._compare(q, output, answer, 3e-4)
734
735    def test_pinhole_romberg(self):
736        """
737        Compare pinhole resolution smearing with romberg integration result.
738        """
739        pars = TEST_PARS_PINHOLE_SPHERE
740        data_string = TEST_DATA_PINHOLE_SPHERE
741        pars['radius'] *= 5
742
743        data = np.loadtxt(data_string.split('\n')).T
744        q, q_width, answer = data
745        answer = romberg_pinhole_1d(q, q_width, self.model, pars)
746        ## Getting 0.1% requires 5 sigma and 200 points per fringe
747        #q_calc = interpolate(pinhole_extend_q(q, q_width, nsigma=5),
748        #                     2*np.pi/pars['radius']/200)
749        #tol = 0.001
750        ## The default 3 sigma and no extra points gets 1%
751        q_calc = None  # type: np.ndarray
752        tol = 0.01
753        resolution = Pinhole1D(q, q_width, q_calc=q_calc)
754        output = self._eval_sphere(pars, resolution)
755        if 0: # debug plot
756            import matplotlib.pyplot as plt  # type: ignore
757            resolution = Perfect1D(q)
758            source = self._eval_sphere(pars, resolution)
759            plt.loglog(q, source, '.')
760            plt.loglog(q, answer, '-', hold=True)
761            plt.loglog(q, output, '-', hold=True)
762            plt.show()
763        self._compare(q, output, answer, tol)
764
765    def test_slit(self):
766        """
767        Compare slit resolution smearing with NIST Igor SANS
768        """
769        pars = TEST_PARS_SLIT_SPHERE
770        data_string = TEST_DATA_SLIT_SPHERE
771
772        data = np.loadtxt(data_string.split('\n')).T
773        q, delta_qv, _, answer = data
774        resolution = Slit1D(q, qx_width=delta_qv, qy_width=0)
775        output = self._eval_sphere(pars, resolution)
776        # TODO: eliminate Igor test since it is too inaccurate to be useful.
777        # This means we can eliminate the test data as well, and instead
778        # use a generated q vector.
779        self._compare(q, output, answer, 0.5)
780
781    def test_slit_romberg(self):
782        """
783        Compare slit resolution smearing with romberg integration result.
784        """
785        pars = TEST_PARS_SLIT_SPHERE
786        data_string = TEST_DATA_SLIT_SPHERE
787
788        data = np.loadtxt(data_string.split('\n')).T
789        q, delta_qv, _, answer = data
790        answer = romberg_slit_1d(q, delta_qv, 0., self.model, pars)
791        q_calc = slit_extend_q(interpolate(q, 2*np.pi/pars['radius']/20),
792                               delta_qv[0], 0.)
793        resolution = Slit1D(q, qx_width=delta_qv, qy_width=0, q_calc=q_calc)
794        output = self._eval_sphere(pars, resolution)
795        # TODO: relative error should be lower
796        self._compare(q, output, answer, 0.025)
797
798    def test_ellipsoid(self):
799        """
800        Compare romberg integration for ellipsoid model.
801        """
802        from .core import load_model
803        pars = {
804            'scale':0.05,
805            'radius_polar':500, 'radius_equatorial':15000,
806            'sld':6, 'sld_solvent': 1,
807            }
808        form = load_model('ellipsoid', dtype='double')
809        q = np.logspace(log10(4e-5), log10(2.5e-2), 68)
810        width, height = 0.117, 0.
811        resolution = Slit1D(q, qx_width=width, qy_width=height)
812        answer = romberg_slit_1d(q, width, height, form, pars)
813        output = resolution.apply(eval_form(resolution.q_calc, form, pars))
814        # TODO: 10% is too much error; use better algorithm
815        #print(np.max(abs(answer-output)/answer))
816        self._compare(q, output, answer, 0.1)
817
818    #TODO: can sas q spacing be too sparse for the resolution calculation?
819    @unittest.skip("suppress sparse data test; not supported by current code")
820    def test_pinhole_sparse(self):
821        """
822        Compare pinhole resolution smearing with NIST Igor SANS on sparse data
823        """
824        pars = TEST_PARS_PINHOLE_SPHERE
825        data_string = TEST_DATA_PINHOLE_SPHERE
826
827        data = np.loadtxt(data_string.split('\n')).T
828        q, q_width, answer = data[:, ::20] # Take every nth point
829        resolution = Pinhole1D(q, q_width)
830        output = self._eval_sphere(pars, resolution)
831        self._compare(q, output, answer, 1e-6)
832
833
834# pinhole sphere parameters
835TEST_PARS_PINHOLE_SPHERE = {
836    'scale': 1.0, 'background': 0.01,
837    'radius': 60.0, 'sld': 1, 'sld_solvent': 6.3,
838    }
839# Q, dQ, I(Q) calculated by NIST Igor SANS package
840TEST_DATA_PINHOLE_SPHERE = """\
8410.001278 0.0002847 2538.41176383
8420.001562 0.0002905 2536.91820405
8430.001846 0.0002956 2535.13182479
8440.002130 0.0003017 2533.06217813
8450.002414 0.0003087 2530.70378586
8460.002698 0.0003165 2528.05024192
8470.002982 0.0003249 2525.10408349
8480.003266 0.0003340 2521.86667499
8490.003550 0.0003437 2518.33907750
8500.003834 0.0003539 2514.52246995
8510.004118 0.0003646 2510.41798319
8520.004402 0.0003757 2506.02690988
8530.004686 0.0003872 2501.35067884
8540.004970 0.0003990 2496.38678318
8550.005253 0.0004112 2491.16237596
8560.005537 0.0004237 2485.63911673
8570.005821 0.0004365 2479.83657083
8580.006105 0.0004495 2473.75676948
8590.006389 0.0004628 2467.40145990
8600.006673 0.0004762 2460.77293372
8610.006957 0.0004899 2453.86724627
8620.007241 0.0005037 2446.69623838
8630.007525 0.0005177 2439.25775219
8640.007809 0.0005318 2431.55421398
8650.008093 0.0005461 2423.58785521
8660.008377 0.0005605 2415.36158137
8670.008661 0.0005750 2406.87009473
8680.008945 0.0005896 2398.12841186
8690.009229 0.0006044 2389.13360806
8700.009513 0.0006192 2379.88958042
8710.009797 0.0006341 2370.39776774
8720.010080 0.0006491 2360.69528793
8730.010360 0.0006641 2350.85169027
8740.010650 0.0006793 2340.42023633
8750.010930 0.0006945 2330.11206013
8760.011220 0.0007097 2319.20109972
8770.011500 0.0007251 2308.43503981
8780.011780 0.0007404 2297.44820179
8790.012070 0.0007558 2285.83853677
8800.012350 0.0007713 2274.41290746
8810.012640 0.0007868 2262.36219581
8820.012920 0.0008024 2250.51169731
8830.013200 0.0008180 2238.45596231
8840.013490 0.0008336 2225.76495666
8850.013770 0.0008493 2213.29618391
8860.014060 0.0008650 2200.19110751
8870.014340 0.0008807 2187.34050325
8880.014620 0.0008965 2174.30529864
8890.014910 0.0009123 2160.61632548
8900.015190 0.0009281 2147.21038112
8910.015470 0.0009440 2133.62023580
8920.015760 0.0009598 2119.37907426
8930.016040 0.0009757 2105.45234903
8940.016330 0.0009916 2090.86319102
8950.016610 0.0010080 2076.60576032
8960.016890 0.0010240 2062.19214565
8970.017180 0.0010390 2047.10550219
8980.017460 0.0010550 2032.38715621
8990.017740 0.0010710 2017.52560123
9000.018030 0.0010880 2001.99124318
9010.018310 0.0011040 1986.84662060
9020.018600 0.0011200 1971.03389745
9030.018880 0.0011360 1955.61395119
9040.019160 0.0011520 1940.08291563
9050.019450 0.0011680 1923.87672225
9060.019730 0.0011840 1908.10656374
9070.020020 0.0012000 1891.66297192
9080.020300 0.0012160 1875.66789021
9090.020580 0.0012320 1859.56357196
9100.020870 0.0012490 1842.79468290
9110.021150 0.0012650 1826.50064489
9120.021430 0.0012810 1810.11533702
9130.021720 0.0012970 1793.06840882
9140.022000 0.0013130 1776.51153580
9150.022280 0.0013290 1759.87201249
9160.022570 0.0013460 1742.57354412
9170.022850 0.0013620 1725.79397319
9180.023140 0.0013780 1708.35831550
9190.023420 0.0013940 1691.45256069
9200.023700 0.0014110 1674.48561783
9210.023990 0.0014270 1656.86525366
9220.024270 0.0014430 1639.79847285
9230.024550 0.0014590 1622.68887088
9240.024840 0.0014760 1604.96421100
9250.025120 0.0014920 1587.85768129
9260.025410 0.0015080 1569.99297335
9270.025690 0.0015240 1552.84580279
9280.025970 0.0015410 1535.54074115
9290.026260 0.0015570 1517.75249337
9300.026540 0.0015730 1500.40115023
9310.026820 0.0015900 1483.03632237
9320.027110 0.0016060 1465.05942429
9330.027390 0.0016220 1447.67682181
9340.027670 0.0016390 1430.46495191
9350.027960 0.0016550 1412.49232282
9360.028240 0.0016710 1395.13182318
9370.028520 0.0016880 1377.93439837
9380.028810 0.0017040 1359.99528971
9390.029090 0.0017200 1342.67274512
9400.029370 0.0017370 1325.55375609
941"""
942
943# Slit sphere parameters
944TEST_PARS_SLIT_SPHERE = {
945    'scale': 0.01, 'background': 0.01,
946    'radius': 60000, 'sld': 1, 'sld_solvent': 4,
947    }
948# Q dQ I(Q) I_smeared(Q)
949TEST_DATA_SLIT_SPHERE = """\
9502.26097e-05 0.117 5.5781372896e+09 1.4626077708e+06
9512.53847e-05 0.117 5.0363141458e+09 1.3117318023e+06
9522.81597e-05 0.117 4.4850108103e+09 1.1594863713e+06
9533.09347e-05 0.117 3.9364658459e+09 1.0094881630e+06
9543.37097e-05 0.117 3.4019975074e+09 8.6518941303e+05
9553.92597e-05 0.117 2.4139519814e+09 6.0232158311e+05
9564.48097e-05 0.117 1.5816877820e+09 3.8739994090e+05
9575.03597e-05 0.117 9.3715407224e+08 2.2745304775e+05
9585.59097e-05 0.117 4.8387917428e+08 1.2101295768e+05
9596.14597e-05 0.117 2.0193586928e+08 6.0055107771e+04
9606.70097e-05 0.117 5.5886110911e+07 3.2749521065e+04
9617.25597e-05 0.117 3.7782348010e+06 2.6350963616e+04
9627.81097e-05 0.117 5.3407817904e+06 2.9624963314e+04
9638.36597e-05 0.117 2.7975485523e+07 3.4403962254e+04
9648.92097e-05 0.117 4.9845448282e+07 3.6130017903e+04
9659.47597e-05 0.117 6.0092588905e+07 3.3495107849e+04
9661.00310e-04 0.117 5.6823430831e+07 2.7475726279e+04
9671.05860e-04 0.117 4.3857024036e+07 2.0144282226e+04
9681.11410e-04 0.117 2.7277144760e+07 1.3647403260e+04
9691.22510e-04 0.117 3.3119334113e+06 6.6519711526e+03
9701.33610e-04 0.117 1.4412859402e+06 6.9726212813e+03
9711.44710e-04 0.117 8.5620162463e+06 8.1441335775e+03
9721.55810e-04 0.117 9.6957429033e+06 6.4559996521e+03
9731.66910e-04 0.117 4.3818341914e+06 3.6252154156e+03
9741.78010e-04 0.117 2.7448997387e+05 2.4006505342e+03
9751.89110e-04 0.117 8.0472009936e+05 2.8187789089e+03
9762.00210e-04 0.117 2.8149552834e+06 3.0915662855e+03
9772.11310e-04 0.117 2.7510907861e+06 2.3722530293e+03
9782.22410e-04 0.117 1.0053133293e+06 1.4473468311e+03
9792.33510e-04 0.117 5.8428305052e+03 1.2048540556e+03
9802.44610e-04 0.117 5.1699305004e+05 1.4625670042e+03
9812.55710e-04 0.117 1.2120227268e+06 1.5010705973e+03
9822.66810e-04 0.117 9.7896842846e+05 1.1336343426e+03
9832.77910e-04 0.117 2.5507264791e+05 8.1848818080e+02
9843.05660e-04 0.117 5.2403101181e+05 7.4913374357e+02
9853.33410e-04 0.117 5.8699343809e+04 4.4669964560e+02
9863.61160e-04 0.117 3.0844327150e+05 4.6774007542e+02
9873.88910e-04 0.117 8.3360142970e+03 2.7169550220e+02
9884.16660e-04 0.117 1.8630080583e+05 3.0710983679e+02
9894.44410e-04 0.117 3.1616804732e-01 1.7959006831e+02
9904.72160e-04 0.117 1.1299016314e+05 2.0763952339e+02
9914.99910e-04 0.117 2.9952522747e+03 1.2536542765e+02
9925.27660e-04 0.117 6.7625695649e+04 1.4013969777e+02
9935.55410e-04 0.117 7.6927460089e+03 8.2145593180e+01
9946.10910e-04 0.117 1.1229057779e+04 8.4519745643e+01
9956.66410e-04 0.117 1.3035567943e+04 8.1554625609e+01
9967.21910e-04 0.117 1.3309931343e+04 7.4437319172e+01
9977.77410e-04 0.117 1.2462626212e+04 6.4697088261e+01
9988.32910e-04 0.117 1.0912927143e+04 5.3773301044e+01
9998.88410e-04 0.117 9.0172597469e+03 4.2843375753e+01
10009.43910e-04 0.117 7.0496495917e+03 3.2771032724e+01
10019.99410e-04 0.117 5.2030483682e+03 2.4113557144e+01
10021.05491e-03 0.117 3.5988976711e+03 1.7160773658e+01
10031.11041e-03 0.117 2.2996060652e+03 1.2016626459e+01
10041.22141e-03 0.117 6.4766590598e+02 6.0373017740e+00
10051.33241e-03 0.117 4.1963483264e+01 4.5215452974e+00
10061.44341e-03 0.117 6.3370708246e+01 5.1054681903e+00
10071.55441e-03 0.117 3.0736750577e+02 5.9176165298e+00
10081.66541e-03 0.117 5.0327682399e+02 5.9815000189e+00
10091.77641e-03 0.117 5.4084331454e+02 5.1634639625e+00
10101.88741e-03 0.117 4.3488671756e+02 3.8535158148e+00
10111.99841e-03 0.117 2.6322287860e+02 2.5824997753e+00
10122.10941e-03 0.117 1.0793633150e+02 1.7315517194e+00
10132.22041e-03 0.117 1.8474448850e+01 1.4077213604e+00
10142.33141e-03 0.117 1.5864062279e+00 1.4771560682e+00
10152.44241e-03 0.117 3.2267213848e+01 1.6916253448e+00
10162.55341e-03 0.117 7.4289116207e+01 1.8274751193e+00
10172.66441e-03 0.117 9.9000521929e+01 1.7706812289e+00
1018"""
1019
1020def main():
1021    """
1022    Run tests given is sys.argv.
1023
1024    Returns 0 if success or 1 if any tests fail.
1025    """
1026    import sys
1027    import xmlrunner  # type: ignore
1028
1029    suite = unittest.TestSuite()
1030    suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]))
1031
1032    runner = xmlrunner.XMLTestRunner(output='logs')
1033    result = runner.run(suite)
1034    return 1 if result.failures or result.errors else 0
1035
1036
1037############################################################################
1038# usage demo
1039############################################################################
1040
1041def _eval_demo_1d(resolution, title):
1042    import sys
1043    from sasmodels import core
1044    from sasmodels import direct_model
1045    name = sys.argv[1] if len(sys.argv) > 1 else 'cylinder'
1046
1047    if name == 'cylinder':
1048        pars = {'length':210, 'radius':500, 'background': 0}
1049    elif name == 'teubner_strey':
1050        pars = {'a2':0.003, 'c1':-1e4, 'c2':1e10, 'background':0.312643}
1051    elif name == 'sphere' or name == 'spherepy':
1052        pars = TEST_PARS_SLIT_SPHERE
1053    elif name == 'ellipsoid':
1054        pars = {
1055            'scale':0.05, 'background': 0,
1056            'r_polar':500, 'r_equatorial':15000,
1057            'sld':6, 'sld_solvent': 1,
1058            }
1059    else:
1060        pars = {}
1061    model_info = core.load_model_info(name)
1062    model = core.build_model(model_info)
1063
1064    kernel = model.make_kernel([resolution.q_calc])
1065    theory = direct_model.call_kernel(kernel, pars)
1066    Iq = resolution.apply(theory)
1067
1068    if isinstance(resolution, Slit1D):
1069        width, height = resolution.dqx, resolution.dqy
1070        Iq_romb = romberg_slit_1d(resolution.q, width, height, model, pars)
1071    else:
1072        dq = resolution.q_width
1073        Iq_romb = romberg_pinhole_1d(resolution.q, dq, model, pars)
1074
1075    import matplotlib.pyplot as plt  # type: ignore
1076    plt.loglog(resolution.q_calc, theory, label='unsmeared')
1077    plt.loglog(resolution.q, Iq, label='smeared', hold=True)
1078    plt.loglog(resolution.q, Iq_romb, label='romberg smeared', hold=True)
1079    plt.legend()
1080    plt.title(title)
1081    plt.xlabel("Q (1/Ang)")
1082    plt.ylabel("I(Q) (1/cm)")
1083
1084def demo_pinhole_1d():
1085    """
1086    Show example of pinhole smearing.
1087    """
1088    q = np.logspace(-4, np.log10(0.2), 400)
1089    q_width = 0.1*q
1090    resolution = Pinhole1D(q, q_width)
1091    _eval_demo_1d(resolution, title="10% dQ/Q Pinhole Resolution")
1092
1093def demo_slit_1d():
1094    """
1095    Show example of slit smearing.
1096    """
1097    q = np.logspace(-4, np.log10(0.2), 100)
1098    w = h = 0.
1099    #w = 0.000000277790
1100    w = 0.0277790
1101    #h = 0.00277790
1102    #h = 0.0277790
1103    resolution = Slit1D(q, w, h)
1104    _eval_demo_1d(resolution, title="(%g,%g) Slit Resolution"%(w, h))
1105
1106def demo():
1107    """
1108    Run the resolution demos.
1109    """
1110    import matplotlib.pyplot as plt  # type: ignore
1111    plt.subplot(121)
1112    demo_pinhole_1d()
1113    #plt.yscale('linear')
1114    plt.subplot(122)
1115    demo_slit_1d()
1116    #plt.yscale('linear')
1117    plt.show()
1118
1119
1120if __name__ == "__main__":
1121    #demo()
1122    main()
Note: See TracBrowser for help on using the repository browser.