source: sasmodels/sasmodels/resolution.py @ 33c8d73

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

linting

  • Property mode set to 100644
File size: 16.8 KB
Line 
1"""
2Define the resolution functions for the data.
3
4This defines classes for 1D and 2D resolution calculations.
5"""
6from scipy.special import erf
7from numpy import sqrt
8import numpy as np
9
10SLIT_SMEAR_POINTS = 500
11MINIMUM_RESOLUTION = 1e-8
12# TODO: Q_EXTEND_STEPS is much bigger than necessary
13Q_EXTEND_STEPS = 30   # number of extra q points above and below
14
15
16class Resolution1D(object):
17    """
18    Abstract base class defining a 1D resolution function.
19
20    *q* is the set of q values at which the data is measured.
21
22    *q_calc* is the set of q values at which the theory needs to be evaluated.
23    This may extend and interpolate the q values.
24
25    *apply* is the method to call with I(q_calc) to compute the resolution
26    smeared theory I(q).
27    """
28    q = None
29    q_calc = None
30    def apply(self, theory):
31        """
32        Smear *theory* by the resolution function, returning *Iq*.
33        """
34        raise NotImplementedError("Subclass does not define the apply function")
35
36class Perfect1D(Resolution1D):
37    """
38    Resolution function to use when there is no actual resolution smearing
39    to be applied.  It has the same interface as the other resolution
40    functions, but returns the identity function.
41    """
42    def __init__(self, q):
43        self.q_calc = self.q = q
44
45    def apply(self, theory):
46        return theory
47
48class Pinhole1D(Resolution1D):
49    r"""
50    Pinhole aperture with q-dependent gaussian resolution.
51
52    *q* points at which the data is measured.
53
54    *q_width* gaussian 1-sigma resolution at each data point.
55
56    *q_calc* is the list of points to calculate, or None if this should
57    be autogenerated from the *q,q_width*.
58    """
59    def __init__(self, q, q_width, q_calc=None):
60        #TODO: maybe add min_step=np.inf
61        #*min_step* is the minimum point spacing to use when computing the
62        #underlying model.  It should be on the order of
63        #$\tfrac{1}{10}\tfrac{2\pi}{d_\text{max}}$ to make sure that fringes
64        #are computed with sufficient density to avoid aliasing effects.
65
66        # Protect against calls with q_width=0.  The extend_q function will
67        # not extend the q if q_width is 0, but q_width must be non-zero when
68        # constructing the weight matrix to avoid division by zero errors.
69        # In practice this should never be needed, since resolution should
70        # default to Perfect1D if the pinhole geometry is not defined.
71        self.q, self.q_width = q, q_width
72        self.q_calc = pinhole_extend_q(q, q_width) if q_calc is None else q_calc
73        self.weight_matrix = pinhole_resolution(self.q_calc,
74                self.q, np.maximum(q_width, MINIMUM_RESOLUTION))
75
76    def apply(self, theory):
77        return apply_resolution_matrix(self.weight_matrix, theory)
78
79class Slit1D(Resolution1D):
80    """
81    Slit aperture with a complicated resolution function.
82
83    *q* points at which the data is measured.
84
85    *qx_width* slit width
86
87    *qy_height* slit height
88    """
89    def __init__(self, q, qx_width, qy_width):
90        if np.isscalar(qx_width):
91            qx_width = qx_width*np.ones(len(q))
92        if np.isscalar(qy_width):
93            qy_width = qy_width*np.ones(len(q))
94        self.q, self.qx_width, self.qy_width = [
95            v.flatten() for v in (q, qx_width, qy_width)]
96        self.q_calc = slit_extend_q(q, qx_width, qy_width)
97        self.weight_matrix = \
98            slit_resolution(self.q_calc, self.q, self.qx_width, self.qy_width)
99
100    def apply(self, theory):
101        return apply_resolution_matrix(self.weight_matrix, theory)
102
103
104def apply_resolution_matrix(weight_matrix, theory):
105    """
106    Apply the resolution weight matrix to the computed theory function.
107    """
108    #print "apply shapes",theory.shape, self.weight_matrix.shape
109    Iq = np.dot(theory[None,:], weight_matrix)
110    #print "result shape",Iq.shape
111    return Iq.flatten()
112
113def pinhole_resolution(q_calc, q, q_width):
114    """
115    Compute the convolution matrix *W* for pinhole resolution 1-D data.
116
117    Each row *W[i]* determines the normalized weight that the corresponding
118    points *q_calc* contribute to the resolution smeared point *q[i]*.  Given
119    *W*, the resolution smearing can be computed using *dot(W,q)*.
120
121    *q_calc* must be increasing.
122    """
123    edges = bin_edges(q_calc)
124    edges[edges<0.0] = 0.0 # clip edges below zero
125    index = (q_width > 0.0) # treat perfect resolution differently
126    G = erf( (edges[:,None] - q[None,:]) / (sqrt(2.0)*q_width)[None,:] )
127    weights = G[1:] - G[:-1]
128    # w = np.sum(weights, axis=0); print "w",w.shape, w
129    weights /= np.sum(weights, axis=0)[None,:]
130    return weights
131
132
133def pinhole_extend_q(q, q_width):
134    """
135    Given *q* and *q_width*, find a set of sampling points *q_calc* so
136    that each point I(q) has sufficient support from the underlying
137    function.
138    """
139    # If using min_step, you could do something like:
140    #     q_calc = np.arange(min, max, min_step)
141    #     index = np.searchsorted(q_calc, q)
142    #     q_calc[index] = q
143    # A refinement would be to assign q to the nearest neighbour.  A further
144    # refinement would be to guard multiple q points between q_calc points,
145    # either by removing duplicates or by only moving the values nearest the
146    # edges.  For really sparse samples, you will want to remove all remaining
147    # points that are not within three standard deviations of a measured q.
148    q_min, q_max = np.min(q), np.max(q)
149    extended_min, extended_max = np.min(q - 3*q_width), np.max(q + 3*q_width)
150    nbins_low, nbins_high = Q_EXTEND_STEPS, Q_EXTEND_STEPS
151    if extended_min < q_min:
152        q_low = np.linspace(extended_min, q_min, nbins_low+1)[:-1]
153        q_low = q_low[q_low>0.0]
154    else:
155        q_low = []
156    if extended_max > q_max:
157        q_high = np.linspace(q_max, extended_max, nbins_high+1)[:-1]
158    else:
159        q_high = []
160    return np.concatenate([q_low, q, q_high])
161
162
163def slit_resolution(q_calc, q, qx_width, qy_width):
164    edges = bin_edges(q_calc) # Note: requires q > 0
165    edges[edges<0.0] = 0.0 # clip edges below zero
166    qy_min, qy_max = 0.0, edges[-1]
167
168    # Make q_calc into a row vector, and q, qx_width, qy_width into columns
169    # Make weights match [ q_calc X q ]
170    weights = np.zeros((len(q),len(q_calc)), 'd')
171    q_calc = q_calc[None,:]
172    q, qx_width, qy_width, edges = [
173        v[:,None] for v in (q, qx_width, qy_width, edges)]
174
175    # Loop for width (height is analytical).
176    # Condition: height >>> width, otherwise, below is not accurate enough.
177    # Smear weight numerical iteration for width>0 when height>0.
178    # When width = 0, the numerical iteration will be skipped.
179    # The resolution calculation for the height is done by direct integration,
180    # assuming the I(q'=sqrt(q_j^2-(q+shift_w)^2)) is constant within
181    # a q' bin, [q_high, q_low].
182    # In general, this weight numerical iteration for width>0 might be a rough
183    # approximation, but it must be good enough when height >>> width.
184    E_sq = edges**2
185    y_points = SLIT_SMEAR_POINTS if np.any(qy_width>0) else 1
186    qy_step = 0 if y_points == 1 else qy_width/(y_points-1)
187    for k in range(-y_points+1,y_points):
188        qy = np.clip(q + qy_step*k, qy_min, qy_max)
189        qx_low = qy
190        qx_high = sqrt(qx_low**2 + qx_width**2)
191        in_x = (q_calc>=qx_low)*(q_calc<=qx_high)
192        qy_sq = qy**2
193        weights += (sqrt(E_sq[1:]-qy_sq) - sqrt(qy_sq - E_sq[:-1]))*in_x
194    #w = np.sum(weights, axis=1); print "w",w.shape, w
195    weights /= np.sum(weights, axis=1)[:,None]
196    return weights
197
198
199def slit_extend_q(q, qx_width, qy_width):
200    return q
201
202
203def bin_edges(x):
204    """
205    Determine bin edges from bin centers, assuming that edges are centered
206    between the bins.
207
208    Note: this uses the arithmetic mean, which may not be appropriate for
209    log-scaled data.
210    """
211    if len(x) < 2 or (np.diff(x)<0).any():
212        raise ValueError("Expected bins to be an increasing set")
213    edges = np.hstack([
214        x[0]  - 0.5*(x[1]  - x[0]),  # first point minus half first interval
215        0.5*(x[1:] + x[:-1]),        # mid points of all central intervals
216        x[-1] + 0.5*(x[-1] - x[-2]), # last point plus half last interval
217        ])
218    return edges
219
220############################################################################
221# unit tests
222############################################################################
223import unittest
224
225class ResolutionTest(unittest.TestCase):
226
227    def setUp(self):
228        self.x = 0.001*np.arange(1, 11)
229        self.y = self.Iq(self.x)
230
231    def Iq(self, q):
232        "Linear function for resolution unit test"
233        return 12.0 - 1000.0*q
234
235    def test_perfect(self):
236        """
237        Perfect resolution and no smearing.
238        """
239        resolution = Perfect1D(self.x)
240        theory = self.Iq(resolution.q_calc)
241        output = resolution.apply(theory)
242        np.testing.assert_equal(output, self.y)
243
244    def test_slit_zero(self):
245        """
246        Slit smearing with perfect resolution.
247        """
248        resolution = Slit1D(self.x, 0., 0.)
249        theory = self.Iq(resolution.q_calc)
250        output = resolution.apply(theory)
251        np.testing.assert_equal(output, self.y)
252
253    @unittest.skip("slit smearing is still broken")
254    def test_slit(self):
255        """
256        Slit smearing with height 0.005
257        """
258        resolution = Slit1D(self.x, 0., 0.005)
259        theory = self.Iq(resolution.q_calc)
260        output = resolution.apply(theory)
261        answer = [ 9.0618, 8.6401, 8.1186, 7.1391, 6.1528,
262                   5.5555, 4.5584, 3.5606, 2.5623, 2. ]
263        np.testing.assert_allclose(output, answer, atol=1e-4)
264
265    def test_pinhole_zero(self):
266        """
267        Pinhole smearing with perfect resolution
268        """
269        resolution = Pinhole1D(self.x, 0.0*self.x)
270        theory = self.Iq(resolution.q_calc)
271        output = resolution.apply(theory)
272        np.testing.assert_equal(output, self.y)
273
274    def test_pinhole(self):
275        """
276        Pinhole smearing with dQ = 0.001 [Note: not dQ/Q = 0.001]
277        """
278        resolution = Pinhole1D(self.x, 0.001*np.ones_like(self.x),
279                               q_calc=self.x)
280        theory = 12.0-1000.0*resolution.q_calc
281        output = resolution.apply(theory)
282        answer = [ 10.44785079, 9.84991299, 8.98101708,
283                  7.99906585, 6.99998311, 6.00001689,
284                  5.00093415, 4.01898292, 3.15008701, 2.55214921]
285        np.testing.assert_allclose(output, answer, atol=1e-8)
286
287# Q, dQ, I(Q) for Igor default sphere model.
288# combines CMSphere5.txt and CMSphere5smearsphere.txt from sasview/tests
289# TODO: move test data into its own file?
290SPHERE_RESOLUTION_TEST_DATA = """\
2910.001278 0.0002847 2538.41176383
2920.001562 0.0002905 2536.91820405
2930.001846 0.0002956 2535.13182479
2940.002130 0.0003017 2533.06217813
2950.002414 0.0003087 2530.70378586
2960.002698 0.0003165 2528.05024192
2970.002982 0.0003249 2525.10408349
2980.003266 0.0003340 2521.86667499
2990.003550 0.0003437 2518.33907750
3000.003834 0.0003539 2514.52246995
3010.004118 0.0003646 2510.41798319
3020.004402 0.0003757 2506.02690988
3030.004686 0.0003872 2501.35067884
3040.004970 0.0003990 2496.38678318
3050.005253 0.0004112 2491.16237596
3060.005537 0.0004237 2485.63911673
3070.005821 0.0004365 2479.83657083
3080.006105 0.0004495 2473.75676948
3090.006389 0.0004628 2467.40145990
3100.006673 0.0004762 2460.77293372
3110.006957 0.0004899 2453.86724627
3120.007241 0.0005037 2446.69623838
3130.007525 0.0005177 2439.25775219
3140.007809 0.0005318 2431.55421398
3150.008093 0.0005461 2423.58785521
3160.008377 0.0005605 2415.36158137
3170.008661 0.0005750 2406.87009473
3180.008945 0.0005896 2398.12841186
3190.009229 0.0006044 2389.13360806
3200.009513 0.0006192 2379.88958042
3210.009797 0.0006341 2370.39776774
3220.010080 0.0006491 2360.69528793
3230.010360 0.0006641 2350.85169027
3240.010650 0.0006793 2340.42023633
3250.010930 0.0006945 2330.11206013
3260.011220 0.0007097 2319.20109972
3270.011500 0.0007251 2308.43503981
3280.011780 0.0007404 2297.44820179
3290.012070 0.0007558 2285.83853677
3300.012350 0.0007713 2274.41290746
3310.012640 0.0007868 2262.36219581
3320.012920 0.0008024 2250.51169731
3330.013200 0.0008180 2238.45596231
3340.013490 0.0008336 2225.76495666
3350.013770 0.0008493 2213.29618391
3360.014060 0.0008650 2200.19110751
3370.014340 0.0008807 2187.34050325
3380.014620 0.0008965 2174.30529864
3390.014910 0.0009123 2160.61632548
3400.015190 0.0009281 2147.21038112
3410.015470 0.0009440 2133.62023580
3420.015760 0.0009598 2119.37907426
3430.016040 0.0009757 2105.45234903
3440.016330 0.0009916 2090.86319102
3450.016610 0.0010080 2076.60576032
3460.016890 0.0010240 2062.19214565
3470.017180 0.0010390 2047.10550219
3480.017460 0.0010550 2032.38715621
3490.017740 0.0010710 2017.52560123
3500.018030 0.0010880 2001.99124318
3510.018310 0.0011040 1986.84662060
3520.018600 0.0011200 1971.03389745
3530.018880 0.0011360 1955.61395119
3540.019160 0.0011520 1940.08291563
3550.019450 0.0011680 1923.87672225
3560.019730 0.0011840 1908.10656374
3570.020020 0.0012000 1891.66297192
3580.020300 0.0012160 1875.66789021
3590.020580 0.0012320 1859.56357196
3600.020870 0.0012490 1842.79468290
3610.021150 0.0012650 1826.50064489
3620.021430 0.0012810 1810.11533702
3630.021720 0.0012970 1793.06840882
3640.022000 0.0013130 1776.51153580
3650.022280 0.0013290 1759.87201249
3660.022570 0.0013460 1742.57354412
3670.022850 0.0013620 1725.79397319
3680.023140 0.0013780 1708.35831550
3690.023420 0.0013940 1691.45256069
3700.023700 0.0014110 1674.48561783
3710.023990 0.0014270 1656.86525366
3720.024270 0.0014430 1639.79847285
3730.024550 0.0014590 1622.68887088
3740.024840 0.0014760 1604.96421100
3750.025120 0.0014920 1587.85768129
3760.025410 0.0015080 1569.99297335
3770.025690 0.0015240 1552.84580279
3780.025970 0.0015410 1535.54074115
3790.026260 0.0015570 1517.75249337
3800.026540 0.0015730 1500.40115023
3810.026820 0.0015900 1483.03632237
3820.027110 0.0016060 1465.05942429
3830.027390 0.0016220 1447.67682181
3840.027670 0.0016390 1430.46495191
3850.027960 0.0016550 1412.49232282
3860.028240 0.0016710 1395.13182318
3870.028520 0.0016880 1377.93439837
3880.028810 0.0017040 1359.99528971
3890.029090 0.0017200 1342.67274512
3900.029370 0.0017370 1325.55375609
391"""
392
393class Pinhole1DTest(unittest.TestCase):
394
395    def setUp(self):
396        # sample q, q_width, I(q) calculated by NIST Igor SANS package
397        self.data = np.loadtxt(SPHERE_RESOLUTION_TEST_DATA.split('\n')).T
398        self.pars = dict(scale=1.0, background=0.01, radius=60.0,
399                         solvent_sld=6.3, sld=1)
400
401    def Iq(self, q, q_width):
402        from sasmodels import core
403        from sasmodels.models import sphere
404
405        model = core.load_model(sphere)
406        resolution = Pinhole1D(q, q_width)
407        kernel = core.make_kernel(model, [resolution.q_calc])
408        theory = core.call_kernel(kernel, self.pars)
409        result = resolution.apply(theory)
410        return result
411
412    def test_sphere(self):
413        """
414        Compare pinhole resolution smearing with NIST Igor SANS
415        """
416        q, q_width, answer = self.data
417        output = self.Iq(q, q_width)
418        np.testing.assert_allclose(output, answer, rtol=0.006)
419
420    #TODO: is all sas data sampled densely enough to support resolution calcs?
421    @unittest.skip("suppress sparse data test; not supported by current code")
422    def test_sphere_sparse(self):
423        """
424        Compare pinhole resolution smearing with NIST Igor SANS on sparse data
425        """
426        q, q_width, answer = self.data[:, ::20]  # Take every nth point
427        output = self.Iq(q, q_width)
428        np.testing.assert_allclose(output, answer, rtol=0.006)
429
430
431def main():
432    """
433    Run tests given is sys.argv.
434
435    Returns 0 if success or 1 if any tests fail.
436    """
437    import sys
438    import xmlrunner
439
440    suite = unittest.TestSuite()
441    suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]))
442
443    runner = xmlrunner.XMLTestRunner(output='logs')
444    result = runner.run(suite)
445    return 1 if result.failures or result.errors else 0
446
447
448############################################################################
449# usage demo
450############################################################################
451
452def _eval_demo_1d(resolution, title):
453    from sasmodels import core
454    from sasmodels.models import cylinder
455    ## or alternatively:
456    # cylinder = core.load_model_definition('cylinder')
457    model = core.load_model(cylinder)
458
459    kernel = core.make_kernel(model, [resolution.q_calc])
460    theory = core.call_kernel(kernel, {'length':210, 'radius':500})
461    Iq = resolution.apply(theory)
462
463    import matplotlib.pyplot as plt
464    plt.loglog(resolution.q_calc, theory, label='unsmeared')
465    plt.loglog(resolution.q, Iq, label='smeared', hold=True)
466    plt.legend()
467    plt.title(title)
468    plt.xlabel("Q (1/Ang)")
469    plt.ylabel("I(Q) (1/cm)")
470
471def demo_pinhole_1d():
472    q = np.logspace(-3, -1, 400)
473    q_width = 0.1*q
474    resolution = Pinhole1D(q, q_width)
475    _eval_demo_1d(resolution, title="10% dQ/Q Pinhole Resolution")
476
477def demo_slit_1d():
478    q = np.logspace(-3, -1, 400)
479    qx_width = 0.005
480    qy_width = 0.0
481    resolution = Slit1D(q, qx_width, qy_width)
482    _eval_demo_1d(resolution, title="0.005 Qx Slit Resolution")
483
484def demo():
485    import matplotlib.pyplot as plt
486    plt.subplot(121)
487    demo_pinhole_1d()
488    plt.subplot(122)
489    demo_slit_1d()
490    plt.show()
491
492
493if __name__ == "__main__":
494    #demo()
495    main()
496
497
Note: See TracBrowser for help on using the repository browser.