source: sasmodels/explore/jitter.py @ de71632

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

label the jitter plots with the projection name

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