source: sasmodels/sasmodels/jitter.py @ 42158d2

core_shell_microgelsmagnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 42158d2 was 42158d2, checked in by Paul Kienzle <pkienzle@…>, 6 years ago

work around travis/appveyor limitations for pytest

  • Property mode set to 100755
File size: 28.6 KB
Line 
1#!/usr/bin/env python
2"""
3Jitter Explorer
4===============
5
6Application to explore orientation angle and angular dispersity.
7"""
8from __future__ import division, print_function
9
10import sys, os
11sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
12
13import argparse
14
15try: # CRUFT: travis-ci does not support mpl_toolkits.mplot3d
16    import mpl_toolkits.mplot3d  # Adds projection='3d' option to subplot
17except ImportError:
18    pass
19
20import matplotlib.pyplot as plt
21from matplotlib.widgets import Slider, CheckButtons
22from matplotlib import cm
23import numpy as np
24from numpy import pi, cos, sin, sqrt, exp, degrees, radians
25
26def draw_beam(ax, view=(0, 0)):
27    """
28    Draw the beam going from source at (0, 0, 1) to detector at (0, 0, -1)
29    """
30    #ax.plot([0,0],[0,0],[1,-1])
31    #ax.scatter([0]*100,[0]*100,np.linspace(1, -1, 100), alpha=0.8)
32
33    steps = 25
34    u = np.linspace(0, 2 * np.pi, steps)
35    v = np.linspace(-1, 1, steps)
36
37    r = 0.02
38    x = r*np.outer(np.cos(u), np.ones_like(v))
39    y = r*np.outer(np.sin(u), np.ones_like(v))
40    z = 1.3*np.outer(np.ones_like(u), v)
41
42    theta, phi = view
43    shape = x.shape
44    points = np.matrix([x.flatten(), y.flatten(), z.flatten()])
45    points = Rz(phi)*Ry(theta)*points
46    x, y, z = [v.reshape(shape) for v in points]
47
48    ax.plot_surface(x, y, z, rstride=4, cstride=4, color='y', alpha=0.5)
49
50def draw_jitter(ax, view, jitter, dist='gaussian', size=(0.1, 0.4, 1.0)):
51    """
52    Represent jitter as a set of shapes at different orientations.
53    """
54    # set max diagonal to 0.95
55    scale = 0.95/sqrt(sum(v**2 for v in size))
56    size = tuple(scale*v for v in size)
57    draw_shape = draw_parallelepiped
58    #draw_shape = draw_ellipsoid
59
60    #np.random.seed(10)
61    #cloud = np.random.randn(10,3)
62    cloud = [
63        [-1, -1, -1],
64        [-1, -1,  0],
65        [-1, -1,  1],
66        [-1,  0, -1],
67        [-1,  0,  0],
68        [-1,  0,  1],
69        [-1,  1, -1],
70        [-1,  1,  0],
71        [-1,  1,  1],
72        [ 0, -1, -1],
73        [ 0, -1,  0],
74        [ 0, -1,  1],
75        [ 0,  0, -1],
76        [ 0,  0,  0],
77        [ 0,  0,  1],
78        [ 0,  1, -1],
79        [ 0,  1,  0],
80        [ 0,  1,  1],
81        [ 1, -1, -1],
82        [ 1, -1,  0],
83        [ 1, -1,  1],
84        [ 1,  0, -1],
85        [ 1,  0,  0],
86        [ 1,  0,  1],
87        [ 1,  1, -1],
88        [ 1,  1,  0],
89        [ 1,  1,  1],
90    ]
91    dtheta, dphi, dpsi = jitter
92    if dtheta == 0:
93        cloud = [v for v in cloud if v[0] == 0]
94    if dphi == 0:
95        cloud = [v for v in cloud if v[1] == 0]
96    if dpsi == 0:
97        cloud = [v for v in cloud if v[2] == 0]
98    draw_shape(ax, size, view, [0, 0, 0], steps=100, alpha=0.8)
99    scale = {'gaussian':1, 'rectangle':1/sqrt(3), 'uniform':1/3}[dist]
100    for point in cloud:
101        delta = [scale*dtheta*point[0], scale*dphi*point[1], scale*dpsi*point[2]]
102        draw_shape(ax, size, view, delta, alpha=0.8)
103    for v in 'xyz':
104        a, b, c = size
105        lim = np.sqrt(a**2+b**2+c**2)
106        getattr(ax, 'set_'+v+'lim')([-lim, lim])
107        getattr(ax, v+'axis').label.set_text(v)
108
109def draw_ellipsoid(ax, size, view, jitter, steps=25, alpha=1):
110    """Draw an ellipsoid."""
111    a,b,c = size
112    u = np.linspace(0, 2 * np.pi, steps)
113    v = np.linspace(0, np.pi, steps)
114    x = a*np.outer(np.cos(u), np.sin(v))
115    y = b*np.outer(np.sin(u), np.sin(v))
116    z = c*np.outer(np.ones_like(u), np.cos(v))
117    x, y, z = transform_xyz(view, jitter, x, y, z)
118
119    ax.plot_surface(x, y, z, rstride=4, cstride=4, color='w', alpha=alpha)
120
121    draw_labels(ax, view, jitter, [
122         ('c+', [ 0, 0, c], [ 1, 0, 0]),
123         ('c-', [ 0, 0,-c], [ 0, 0,-1]),
124         ('a+', [ a, 0, 0], [ 0, 0, 1]),
125         ('a-', [-a, 0, 0], [ 0, 0,-1]),
126         ('b+', [ 0, b, 0], [-1, 0, 0]),
127         ('b-', [ 0,-b, 0], [-1, 0, 0]),
128    ])
129
130def draw_parallelepiped(ax, size, view, jitter, steps=None, alpha=1):
131    """Draw a parallelepiped."""
132    a, b, c = size
133    x = a*np.array([ 1,-1, 1,-1, 1,-1, 1,-1])
134    y = b*np.array([ 1, 1,-1,-1, 1, 1,-1,-1])
135    z = c*np.array([ 1, 1, 1, 1,-1,-1,-1,-1])
136    tri = np.array([
137        # counter clockwise triangles
138        # z: up/down, x: right/left, y: front/back
139        [0,1,2], [3,2,1], # top face
140        [6,5,4], [5,6,7], # bottom face
141        [0,2,6], [6,4,0], # right face
142        [1,5,7], [7,3,1], # left face
143        [2,3,6], [7,6,3], # front face
144        [4,1,0], [5,1,4], # back face
145    ])
146
147    x, y, z = transform_xyz(view, jitter, x, y, z)
148    ax.plot_trisurf(x, y, triangles=tri, Z=z, color='w', alpha=alpha)
149
150    # Draw pink face on box.
151    # Since I can't control face color, instead draw a thin box situated just
152    # in front of the "a+" face.
153    if 1:
154        x = a*np.array([ 1,-1, 1,-1, 1,-1, 1,-1])
155        y = b*np.array([ 1, 1,-1,-1, 1, 1,-1,-1])
156        z = c*np.array([ 1, 1, 1, 1,-1,-1,-1,-1])
157        x, y, z = transform_xyz(view, jitter, abs(x)*1.05, y, z)
158        ax.plot_trisurf(x, y, triangles=tri, Z=z, color=[1,0.6,0.6], alpha=alpha)
159
160    draw_labels(ax, view, jitter, [
161         ('c+', [ 0, 0, c], [ 1, 0, 0]),
162         ('c-', [ 0, 0,-c], [ 0, 0,-1]),
163         ('a+', [ a, 0, 0], [ 0, 0, 1]),
164         ('a-', [-a, 0, 0], [ 0, 0,-1]),
165         ('b+', [ 0, b, 0], [-1, 0, 0]),
166         ('b-', [ 0,-b, 0], [-1, 0, 0]),
167    ])
168
169def draw_sphere(ax, radius=10., steps=100):
170    """Draw a sphere"""
171    u = np.linspace(0, 2 * np.pi, steps)
172    v = np.linspace(0, np.pi, steps)
173
174    x = radius * np.outer(np.cos(u), np.sin(v))
175    y = radius * np.outer(np.sin(u), np.sin(v))
176    z = radius * np.outer(np.ones(np.size(u)), np.cos(v))
177    ax.plot_surface(x, y, z, rstride=4, cstride=4, color='w')
178
179PROJECTIONS = [
180    # in order of PROJECTION number; do not change without updating the
181    # constants in kernel_iq.c
182    'equirectangular', 'sinusoidal', 'guyou', 'azimuthal_equidistance',
183    'azimuthal_equal_area',
184]
185def draw_mesh(ax, view, jitter, radius=1.2, n=11, dist='gaussian',
186              projection='equirectangular'):
187    """
188    Draw the dispersion mesh showing the theta-phi orientations at which
189    the model will be evaluated.
190
191    jitter projections
192    <https://en.wikipedia.org/wiki/List_of_map_projections>
193
194    equirectangular (standard latitude-longitude mesh)
195        <https://en.wikipedia.org/wiki/Equirectangular_projection>
196        Allows free movement in phi (around the equator), but theta is
197        limited to +/- 90, and points are cos-weighted. Jitter in phi is
198        uniform in weight along a line of latitude.  With small theta and
199        phi ranging over +/- 180 this forms a wobbling disk.  With small
200        phi and theta ranging over +/- 90 this forms a wedge like a slice
201        of an orange.
202    azimuthal_equidistance (Postel)
203        <https://en.wikipedia.org/wiki/Azimuthal_equidistant_projection>
204        Preserves distance from center, and so is an excellent map for
205        representing a bivariate gaussian on the surface.  Theta and phi
206        operate identically, cutting wegdes from the antipode of the viewing
207        angle.  This unfortunately does not allow free movement in either
208        theta or phi since the orthogonal wobble decreases to 0 as the body
209        rotates through 180 degrees.
210    sinusoidal (Sanson-Flamsteed, Mercator equal-area)
211        <https://en.wikipedia.org/wiki/Sinusoidal_projection>
212        Preserves arc length with latitude, giving bad behaviour at
213        theta near +/- 90.  Theta and phi operate somewhat differently,
214        so a system with a-b-c dtheta-dphi-dpsi will not give the same
215        value as one with b-a-c dphi-dtheta-dpsi, as would be the case
216        for azimuthal equidistance.  Free movement using theta or phi
217        uniform over +/- 180 will work, but not as well as equirectangular
218        phi, with theta being slightly worse.  Computationally it is much
219        cheaper for wide theta-phi meshes since it excludes points which
220        lie outside the sinusoid near theta +/- 90 rather than packing
221        them close together as in equirectangle.  Note that the poles
222        will be slightly overweighted for theta > 90 with the circle
223        from theta at 90+dt winding backwards around the pole, overlapping
224        the circle from theta at 90-dt.
225    Guyou (hemisphere-in-a-square) **not weighted**
226        <https://en.wikipedia.org/wiki/Guyou_hemisphere-in-a-square_projection>
227        With tiling, allows rotation in phi or theta through +/- 180, with
228        uniform spacing.  Both theta and phi allow free rotation, with wobble
229        in the orthogonal direction reasonably well behaved (though not as
230        good as equirectangular phi). The forward/reverse transformations
231        relies on elliptic integrals that are somewhat expensive, so the
232        behaviour has to be very good to justify the cost and complexity.
233        The weighting function for each point has not yet been computed.
234        Note: run the module *guyou.py* directly and it will show the forward
235        and reverse mappings.
236    azimuthal_equal_area  **incomplete**
237        <https://en.wikipedia.org/wiki/Lambert_azimuthal_equal-area_projection>
238        Preserves the relative density of the surface patches.  Not that
239        useful and not completely implemented
240    Gauss-Kreuger **not implemented**
241        <https://en.wikipedia.org/wiki/Transverse_Mercator_projection#Ellipsoidal_transverse_Mercator>
242        Should allow free movement in theta, but phi is distorted.
243    """
244    theta, phi, psi = view
245    dtheta, dphi, dpsi = jitter
246
247    t = np.linspace(-1, 1, n)
248    weights = np.ones_like(t)
249    if dist == 'gaussian':
250        t *= 3
251        weights = exp(-0.5*t**2)
252    elif dist == 'rectangle':
253        # Note: uses sasmodels ridiculous definition of rectangle width
254        t *= sqrt(3)
255    elif dist == 'uniform':
256        pass
257    else:
258        raise ValueError("expected dist to be gaussian, rectangle or uniform")
259
260    if projection == 'equirectangular':  #define PROJECTION 1
261        def rotate(theta_i, phi_j):
262            return Rx(phi_j)*Ry(theta_i)
263        def weight(theta_i, phi_j, wi, wj):
264            return wi*wj*abs(cos(radians(theta_i)))
265    elif projection == 'sinusoidal':  #define PROJECTION 2
266        def rotate(theta_i, phi_j):
267            latitude = theta_i
268            scale = cos(radians(latitude))
269            longitude = phi_j/scale if abs(phi_j) < abs(scale)*180 else 0
270            #print("(%+7.2f, %+7.2f) => (%+7.2f, %+7.2f)"%(theta_i, phi_j, latitude, longitude))
271            return Rx(longitude)*Ry(latitude)
272        def weight(theta_i, phi_j, wi, wj):
273            latitude = theta_i
274            scale = cos(radians(latitude))
275            w = 1 if abs(phi_j) < abs(scale)*180 else 0
276            return w*wi*wj
277    elif projection == 'guyou':  #define PROJECTION 3  (eventually?)
278        def rotate(theta_i, phi_j):
279            from guyou import guyou_invert
280            #latitude, longitude = guyou_invert([theta_i], [phi_j])
281            longitude, latitude = guyou_invert([phi_j], [theta_i])
282            return Rx(longitude[0])*Ry(latitude[0])
283        def weight(theta_i, phi_j, wi, wj):
284            return wi*wj
285    elif projection == 'azimuthal_equidistance':  # Note: Rz Ry, not Rx Ry
286        def rotate(theta_i, phi_j):
287            latitude = sqrt(theta_i**2 + phi_j**2)
288            longitude = degrees(np.arctan2(phi_j, theta_i))
289            #print("(%+7.2f, %+7.2f) => (%+7.2f, %+7.2f)"%(theta_i, phi_j, latitude, longitude))
290            return Rz(longitude)*Ry(latitude)
291        def weight(theta_i, phi_j, wi, wj):
292            # Weighting for each point comes from the integral:
293            #     \int\int I(q, lat, log) sin(lat) dlat dlog
294            # We are doing a conformal mapping from disk to sphere, so we need
295            # a change of variables g(theta, phi) -> (lat, long):
296            #     lat, long = sqrt(theta^2 + phi^2), arctan(phi/theta)
297            # giving:
298            #     dtheta dphi = det(J) dlat dlong
299            # where J is the jacobian from the partials of g. Using
300            #     R = sqrt(theta^2 + phi^2),
301            # then
302            #     J = [[x/R, Y/R], -y/R^2, x/R^2]]
303            # and
304            #     det(J) = 1/R
305            # with the final integral being:
306            #    \int\int I(q, theta, phi) sin(R)/R dtheta dphi
307            #
308            # This does approximately the right thing, decreasing the weight
309            # of each point as you go farther out on the disk, but it hasn't
310            # yet been checked against the 1D integral results. Prior
311            # to declaring this "good enough" and checking that integrals
312            # work in practice, we will examine alternative mappings.
313            #
314            # The issue is that the mapping does not support the case of free
315            # rotation about a single axis correctly, with a small deviation
316            # in the orthogonal axis independent of the first axis.  Like the
317            # usual polar coordiates integration, the integrated sections
318            # form wedges, though at least in this case the wedge cuts through
319            # the entire sphere, and treats theta and phi identically.
320            latitude = sqrt(theta_i**2 + phi_j**2)
321            w = sin(radians(latitude))/latitude if latitude != 0 else 1
322            return w*wi*wj if latitude < 180 else 0
323    elif projection == 'azimuthal_equal_area':
324        def rotate(theta_i, phi_j):
325            R = min(1, sqrt(theta_i**2 + phi_j**2)/180)
326            latitude = 180-degrees(2*np.arccos(R))
327            longitude = degrees(np.arctan2(phi_j, theta_i))
328            #print("(%+7.2f, %+7.2f) => (%+7.2f, %+7.2f)"%(theta_i, phi_j, latitude, longitude))
329            return Rz(longitude)*Ry(latitude)
330        def weight(theta_i, phi_j, wi, wj):
331            latitude = sqrt(theta_i**2 + phi_j**2)
332            w = sin(radians(latitude))/latitude if latitude != 0 else 1
333            return w*wi*wj if latitude < 180 else 0
334    else:
335        raise ValueError("unknown projection %r"%projection)
336
337    # mesh in theta, phi formed by rotating z
338    z = np.matrix([[0], [0], [radius]])
339    points = np.hstack([rotate(theta_i, phi_j)*z
340                        for theta_i in dtheta*t
341                        for phi_j in dphi*t])
342    # select just the active points (i.e., those with phi < 180
343    w = np.array([weight(theta_i, phi_j, wi, wj)
344                  for wi, theta_i in zip(weights, dtheta*t)
345                  for wj, phi_j in zip(weights, dphi*t)])
346    #print(max(w), min(w), min(w[w>0]))
347    points = points[:, w>0]
348    w = w[w>0]
349    w /= max(w)
350
351    if 0: # Kent distribution
352        points = np.hstack([Rx(phi_j)*Ry(theta_i)*z for theta_i in 30*t for phi_j in 60*t])
353        xp, yp, zp = [np.array(v).flatten() for v in points]
354        kappa = max(1e6, radians(dtheta)/(2*pi))
355        beta = 1/max(1e-6, radians(dphi)/(2*pi))/kappa
356        w = exp(kappa*zp) #+ beta*(xp**2 + yp**2)
357        print(kappa, dtheta, radians(dtheta), min(w), max(w), sum(w))
358        #w /= abs(cos(radians(
359        #w /= sum(w)
360
361    # rotate relative to beam
362    points = orient_relative_to_beam(view, points)
363
364    x, y, z = [np.array(v).flatten() for v in points]
365    #plt.figure(2); plt.clf(); plt.hist(z, bins=np.linspace(-1, 1, 51))
366    ax.scatter(x, y, z, c=w, marker='o', vmin=0., vmax=1.)
367
368def draw_labels(ax, view, jitter, text):
369    """
370    Draw text at a particular location.
371    """
372    labels, locations, orientations = zip(*text)
373    px, py, pz = zip(*locations)
374    dx, dy, dz = zip(*orientations)
375
376    px, py, pz = transform_xyz(view, jitter, px, py, pz)
377    dx, dy, dz = transform_xyz(view, jitter, dx, dy, dz)
378
379    # TODO: zdir for labels is broken, and labels aren't appearing.
380    for label, p, zdir in zip(labels, zip(px, py, pz), zip(dx, dy, dz)):
381        zdir = np.asarray(zdir).flatten()
382        ax.text(p[0], p[1], p[2], label, zdir=zdir)
383
384# Definition of rotation matrices comes from wikipedia:
385#    https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations
386def Rx(angle):
387    """Construct a matrix to rotate points about *x* by *angle* degrees."""
388    a = radians(angle)
389    R = [[1, 0, 0],
390         [0, +cos(a), -sin(a)],
391         [0, +sin(a), +cos(a)]]
392    return np.matrix(R)
393
394def Ry(angle):
395    """Construct a matrix to rotate points about *y* by *angle* degrees."""
396    a = radians(angle)
397    R = [[+cos(a), 0, +sin(a)],
398         [0, 1, 0],
399         [-sin(a), 0, +cos(a)]]
400    return np.matrix(R)
401
402def Rz(angle):
403    """Construct a matrix to rotate points about *z* by *angle* degrees."""
404    a = radians(angle)
405    R = [[+cos(a), -sin(a), 0],
406         [+sin(a), +cos(a), 0],
407         [0, 0, 1]]
408    return np.matrix(R)
409
410def transform_xyz(view, jitter, x, y, z):
411    """
412    Send a set of (x,y,z) points through the jitter and view transforms.
413    """
414    x, y, z = [np.asarray(v) for v in (x, y, z)]
415    shape = x.shape
416    points = np.matrix([x.flatten(),y.flatten(),z.flatten()])
417    points = apply_jitter(jitter, points)
418    points = orient_relative_to_beam(view, points)
419    x, y, z = [np.array(v).reshape(shape) for v in points]
420    return x, y, z
421
422def apply_jitter(jitter, points):
423    """
424    Apply the jitter transform to a set of points.
425
426    Points are stored in a 3 x n numpy matrix, not a numpy array or tuple.
427    """
428    dtheta, dphi, dpsi = jitter
429    points = Rx(dphi)*Ry(dtheta)*Rz(dpsi)*points
430    return points
431
432def orient_relative_to_beam(view, points):
433    """
434    Apply the view transform to a set of points.
435
436    Points are stored in a 3 x n numpy matrix, not a numpy array or tuple.
437    """
438    theta, phi, psi = view
439    points = Rz(phi)*Ry(theta)*Rz(psi)*points
440    return points
441
442# translate between number of dimension of dispersity and the number of
443# points along each dimension.
444PD_N_TABLE = {
445    (0, 0, 0): (0, 0, 0),     # 0
446    (1, 0, 0): (100, 0, 0),   # 100
447    (0, 1, 0): (0, 100, 0),
448    (0, 0, 1): (0, 0, 100),
449    (1, 1, 0): (30, 30, 0),   # 900
450    (1, 0, 1): (30, 0, 30),
451    (0, 1, 1): (0, 30, 30),
452    (1, 1, 1): (15, 15, 15),  # 3375
453}
454
455def clipped_range(data, portion=1.0, mode='central'):
456    """
457    Determine range from data.
458
459    If *portion* is 1, use full range, otherwise use the center of the range
460    or the top of the range, depending on whether *mode* is 'central' or 'top'.
461    """
462    if portion == 1.0:
463        return data.min(), data.max()
464    elif mode == 'central':
465        data = np.sort(data.flatten())
466        offset = int(portion*len(data)/2 + 0.5)
467        return data[offset], data[-offset]
468    elif mode == 'top':
469        data = np.sort(data.flatten())
470        offset = int(portion*len(data) + 0.5)
471        return data[offset], data[-1]
472
473def draw_scattering(calculator, ax, view, jitter, dist='gaussian'):
474    """
475    Plot the scattering for the particular view.
476
477    *calculator* is returned from :func:`build_model`.  *ax* are the 3D axes
478    on which the data will be plotted.  *view* and *jitter* are the current
479    orientation and orientation dispersity.  *dist* is one of the sasmodels
480    weight distributions.
481    """
482    if dist == 'uniform':  # uniform is not yet in this branch
483        dist, scale = 'rectangle', 1/sqrt(3)
484    else:
485        scale = 1
486
487    # add the orientation parameters to the model parameters
488    theta, phi, psi = view
489    theta_pd, phi_pd, psi_pd = [scale*v for v in jitter]
490    theta_pd_n, phi_pd_n, psi_pd_n = PD_N_TABLE[(theta_pd>0, phi_pd>0, psi_pd>0)]
491    ## increase pd_n for testing jitter integration rather than simple viz
492    #theta_pd_n, phi_pd_n, psi_pd_n = [5*v for v in (theta_pd_n, phi_pd_n, psi_pd_n)]
493
494    pars = dict(
495        theta=theta, theta_pd=theta_pd, theta_pd_type=dist, theta_pd_n=theta_pd_n,
496        phi=phi, phi_pd=phi_pd, phi_pd_type=dist, phi_pd_n=phi_pd_n,
497        psi=psi, psi_pd=psi_pd, psi_pd_type=dist, psi_pd_n=psi_pd_n,
498    )
499    pars.update(calculator.pars)
500
501    # compute the pattern
502    qx, qy = calculator._data.x_bins, calculator._data.y_bins
503    Iqxy = calculator(**pars).reshape(len(qx), len(qy))
504
505    # scale it and draw it
506    Iqxy = np.log(Iqxy)
507    if calculator.limits:
508        # use limits from orientation (0,0,0)
509        vmin, vmax = calculator.limits
510    else:
511        vmax = Iqxy.max()
512        vmin = vmax*10**-7
513        #vmin, vmax = clipped_range(Iqxy, portion=portion, mode='top')
514    #print("range",(vmin,vmax))
515    #qx, qy = np.meshgrid(qx, qy)
516    if 0:
517        level = np.asarray(255*(Iqxy - vmin)/(vmax - vmin), 'i')
518        level[level<0] = 0
519        colors = plt.get_cmap()(level)
520        ax.plot_surface(qx, qy, -1.1, rstride=1, cstride=1, facecolors=colors)
521    elif 1:
522        ax.contourf(qx/qx.max(), qy/qy.max(), Iqxy, zdir='z', offset=-1.1,
523                    levels=np.linspace(vmin, vmax, 24))
524    else:
525        ax.pcolormesh(qx, qy, Iqxy)
526
527def build_model(model_name, n=150, qmax=0.5, **pars):
528    """
529    Build a calculator for the given shape.
530
531    *model_name* is any sasmodels model.  *n* and *qmax* define an n x n mesh
532    on which to evaluate the model.  The remaining parameters are stored in
533    the returned calculator as *calculator.pars*.  They are used by
534    :func:`draw_scattering` to set the non-orientation parameters in the
535    calculation.
536
537    Returns a *calculator* function which takes a dictionary or parameters and
538    produces Iqxy.  The Iqxy value needs to be reshaped to an n x n matrix
539    for plotting.  See the :class:`sasmodels.direct_model.DirectModel` class
540    for details.
541    """
542    from sasmodels.core import load_model_info, build_model
543    from sasmodels.data import empty_data2D
544    from sasmodels.direct_model import DirectModel
545
546    model_info = load_model_info(model_name)
547    model = build_model(model_info) #, dtype='double!')
548    q = np.linspace(-qmax, qmax, n)
549    data = empty_data2D(q, q)
550    calculator = DirectModel(data, model)
551
552    # stuff the values for non-orientation parameters into the calculator
553    calculator.pars = pars.copy()
554    calculator.pars.setdefault('backgound', 1e-3)
555
556    # fix the data limits so that we can see if the pattern fades
557    # under rotation or angular dispersion
558    Iqxy = calculator(theta=0, phi=0, psi=0, **calculator.pars)
559    Iqxy = np.log(Iqxy)
560    vmin, vmax = clipped_range(Iqxy, 0.95, mode='top')
561    calculator.limits = vmin, vmax+1
562
563    return calculator
564
565def select_calculator(model_name, n=150, size=(10,40,100)):
566    """
567    Create a model calculator for the given shape.
568
569    *model_name* is one of sphere, cylinder, ellipsoid, triaxial_ellipsoid,
570    parallelepiped or bcc_paracrystal. *n* is the number of points to use
571    in the q range.  *qmax* is chosen based on model parameters for the
572    given model to show something intersting.
573
574    Returns *calculator* and tuple *size* (a,b,c) giving minor and major
575    equitorial axes and polar axis respectively.  See :func:`build_model`
576    for details on the returned calculator.
577    """
578    a, b, c = size
579    if model_name == 'sphere':
580        calculator = build_model('sphere', n=n, radius=c)
581        a = b = c
582    elif model_name == 'bcc_paracrystal':
583        calculator = build_model('bcc_paracrystal', n=n, dnn=c,
584                                  d_factor=0.06, radius=40)
585        a = b = c
586    elif model_name == 'cylinder':
587        calculator = build_model('cylinder', n=n, qmax=0.3, radius=b, length=c)
588        a = b
589    elif model_name == 'ellipsoid':
590        calculator = build_model('ellipsoid', n=n, qmax=1.0,
591                                 radius_polar=c, radius_equatorial=b)
592        a = b
593    elif model_name == 'triaxial_ellipsoid':
594        calculator = build_model('triaxial_ellipsoid', n=n, qmax=0.5,
595                                 radius_equat_minor=a,
596                                 radius_equat_major=b,
597                                 radius_polar=c)
598    elif model_name == 'parallelepiped':
599        calculator = build_model('parallelepiped', n=n, a=a, b=b, c=c)
600    else:
601        raise ValueError("unknown model %s"%model_name)
602
603    return calculator, (a, b, c)
604
605SHAPES = [
606    'parallelepiped', 'triaxial_ellipsoid', 'bcc_paracrystal',
607    'cylinder', 'ellipsoid',
608    'sphere',
609 ]
610
611DISTRIBUTIONS = [
612    'gaussian', 'rectangle', 'uniform',
613]
614DIST_LIMITS = {
615    'gaussian': 30,
616    'rectangle': 90/sqrt(3),
617    'uniform': 90,
618}
619
620def run(model_name='parallelepiped', size=(10, 40, 100),
621        dist='gaussian', mesh=30,
622        projection='equirectangular'):
623    """
624    Show an interactive orientation and jitter demo.
625
626    *model_name* is one of: sphere, cylinder, ellipsoid, parallelepiped,
627    triaxial_ellipsoid, or bcc_paracrystal.
628
629    *size* gives the dimensions (a, b, c) of the shape.
630
631    *dist* is the type of dispersition: gaussian, rectangle, or uniform.
632
633    *mesh* is the number of points in the dispersion mesh.
634
635    *projection* is the map projection to use for the mesh: equirectangular,
636    sinusoidal, guyou, azimuthal_equidistance, or azimuthal_equal_area.
637    """
638    # projection number according to 1-order position in list, but
639    # only 1 and 2 are implemented so far.
640    from sasmodels import generate
641    generate.PROJECTION = PROJECTIONS.index(projection) + 1
642    if generate.PROJECTION > 2:
643        print("*** PROJECTION %s not implemented in scattering function ***"%projection)
644        generate.PROJECTION = 2
645
646    # set up calculator
647    calculator, size = select_calculator(model_name, n=150, size=size)
648
649    ## uncomment to set an independent the colour range for every view
650    ## If left commented, the colour range is fixed for all views
651    calculator.limits = None
652
653    ## initial view
654    #theta, dtheta = 70., 10.
655    #phi, dphi = -45., 3.
656    #psi, dpsi = -45., 3.
657    theta, phi, psi = 0, 0, 0
658    dtheta, dphi, dpsi = 0, 0, 0
659
660    ## create the plot window
661    #plt.hold(True)
662    plt.subplots(num=None, figsize=(5.5, 5.5))
663    plt.set_cmap('gist_earth')
664    plt.clf()
665    plt.gcf().canvas.set_window_title(projection)
666    #gs = gridspec.GridSpec(2,1,height_ratios=[4,1])
667    #ax = plt.subplot(gs[0], projection='3d')
668    ax = plt.axes([0.0, 0.2, 1.0, 0.8], projection='3d')
669    try:  # CRUFT: not all versions of matplotlib accept 'square' 3d projection
670        ax.axis('square')
671    except Exception:
672        pass
673
674    axcolor = 'lightgoldenrodyellow'
675
676    ## add control widgets to plot
677    axtheta  = plt.axes([0.1, 0.15, 0.45, 0.04], axisbg=axcolor)
678    axphi = plt.axes([0.1, 0.1, 0.45, 0.04], axisbg=axcolor)
679    axpsi = plt.axes([0.1, 0.05, 0.45, 0.04], axisbg=axcolor)
680    stheta = Slider(axtheta, 'Theta', -90, 90, valinit=theta)
681    sphi = Slider(axphi, 'Phi', -180, 180, valinit=phi)
682    spsi = Slider(axpsi, 'Psi', -180, 180, valinit=psi)
683
684    axdtheta  = plt.axes([0.75, 0.15, 0.15, 0.04], axisbg=axcolor)
685    axdphi = plt.axes([0.75, 0.1, 0.15, 0.04], axisbg=axcolor)
686    axdpsi= plt.axes([0.75, 0.05, 0.15, 0.04], axisbg=axcolor)
687    # Note: using ridiculous definition of rectangle distribution, whose width
688    # in sasmodels is sqrt(3) times the given width.  Divide by sqrt(3) to keep
689    # the maximum width to 90.
690    dlimit = DIST_LIMITS[dist]
691    sdtheta = Slider(axdtheta, 'dTheta', 0, 2*dlimit, valinit=dtheta)
692    sdphi = Slider(axdphi, 'dPhi', 0, 2*dlimit, valinit=dphi)
693    sdpsi = Slider(axdpsi, 'dPsi', 0, 2*dlimit, valinit=dpsi)
694
695    ## callback to draw the new view
696    def update(val, axis=None):
697        view = stheta.val, sphi.val, spsi.val
698        jitter = sdtheta.val, sdphi.val, sdpsi.val
699        # set small jitter as 0 if multiple pd dims
700        dims = sum(v > 0 for v in jitter)
701        limit = [0, 0, 0.5, 5][dims]
702        jitter = [0 if v < limit else v for v in jitter]
703        ax.cla()
704        draw_beam(ax, (0, 0))
705        draw_jitter(ax, view, jitter, dist=dist, size=size)
706        #draw_jitter(ax, view, (0,0,0))
707        draw_mesh(ax, view, jitter, dist=dist, n=mesh, projection=projection)
708        draw_scattering(calculator, ax, view, jitter, dist=dist)
709        plt.gcf().canvas.draw()
710
711    ## bind control widgets to view updater
712    stheta.on_changed(lambda v: update(v,'theta'))
713    sphi.on_changed(lambda v: update(v, 'phi'))
714    spsi.on_changed(lambda v: update(v, 'psi'))
715    sdtheta.on_changed(lambda v: update(v, 'dtheta'))
716    sdphi.on_changed(lambda v: update(v, 'dphi'))
717    sdpsi.on_changed(lambda v: update(v, 'dpsi'))
718
719    ## initialize view
720    update(None, 'phi')
721
722    ## go interactive
723    plt.show()
724
725def main():
726    parser = argparse.ArgumentParser(
727        description="Display jitter",
728        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
729        )
730    parser.add_argument('-p', '--projection', choices=PROJECTIONS, default=PROJECTIONS[0], help='coordinate projection')
731    parser.add_argument('-s', '--size', type=str, default='10,40,100', help='a,b,c lengths')
732    parser.add_argument('-d', '--distribution', choices=DISTRIBUTIONS, default=DISTRIBUTIONS[0], help='jitter distribution')
733    parser.add_argument('-m', '--mesh', type=int, default=30, help='#points in theta-phi mesh')
734    parser.add_argument('shape', choices=SHAPES, nargs='?', default=SHAPES[0], help='oriented shape')
735    opts = parser.parse_args()
736    size = tuple(int(v) for v in opts.size.split(','))
737    run(opts.shape, size=size,
738        mesh=opts.mesh, dist=opts.distribution,
739        projection=opts.projection)
740
741if __name__ == "__main__":
742    main()
Note: See TracBrowser for help on using the repository browser.