source: sasmodels/explore/jitter.py @ d73a5ac

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

doc tweak for jitter

  • Property mode set to 100755
File size: 26.4 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',
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    Guyour (hemisphere-in-a-square)  **not implemented**
207        <https://en.wikipedia.org/wiki/Guyou_hemisphere-in-a-square_projection>
208        Promising.  With tiling should allow rotation in phi or theta
209        through +/- 180, preserving almost disk-like behaviour in either
210        direction (phi rotation will not be as uniform as it is in
211        equirectangular; not sure about theta).  Unfortunately, distortion
212        is not restricted to the corners of the theta-phi mesh, so this will
213        not be as good as the azimuthal equidistance project for gaussian
214        distributions.
215    azimuthal_equal_area  **incomplete**
216        <https://en.wikipedia.org/wiki/Lambert_azimuthal_equal-area_projection>
217        Preserves the relative density of the surface patches.  Not that
218        useful and not completely implemented
219    Gauss-Kreuger **not implemented**
220        <https://en.wikipedia.org/wiki/Transverse_Mercator_projection#Ellipsoidal_transverse_Mercator>
221        Should allow free movement in theta, but phi is distorted.
222    """
223    theta, phi, psi = view
224    dtheta, dphi, dpsi = jitter
225
226    t = np.linspace(-1, 1, n)
227    weights = np.ones_like(t)
228    if dist == 'gaussian':
229        t *= 3
230        weights = exp(-0.5*t**2)
231    elif dist == 'rectangle':
232        # Note: uses sasmodels ridiculous definition of rectangle width
233        t *= sqrt(3)
234    elif dist == 'uniform':
235        pass
236    else:
237        raise ValueError("expected dist to be gaussian, rectangle or uniform")
238
239    if projection == 'equirectangular':
240        def rotate(theta_i, phi_j):
241            return Rx(phi_j)*Ry(theta_i)
242        def weight(theta_i, phi_j, wi, wj):
243            return wi*wj*cos(radians(theta_i))
244    elif projection == 'azimuthal_equidistance':
245        def rotate(theta_i, phi_j):
246            latitude = sqrt(theta_i**2 + phi_j**2)
247            longitude = degrees(np.arctan2(phi_j, theta_i))
248            #print("(%+7.2f, %+7.2f) => (%+7.2f, %+7.2f)"%(theta_i, phi_j, latitude, longitude))
249            return Rz(longitude)*Ry(latitude)
250        def weight(theta_i, phi_j, wi, wj):
251            # Weighting for each point comes from the integral:
252            #     \int\int I(q, lat, log) sin(lat) dlat dlog
253            # We are doing a conformal mapping from disk to sphere, so we need
254            # a change of variables g(theta, phi) -> (lat, long):
255            #     lat, long = sqrt(theta^2 + phi^2), arctan(phi/theta)
256            # giving:
257            #     dtheta dphi = det(J) dlat dlong
258            # where J is the jacobian from the partials of g. Using
259            #     R = sqrt(theta^2 + phi^2),
260            # then
261            #     J = [[x/R, Y/R], -y/R^2, x/R^2]]
262            # and
263            #     det(J) = 1/R
264            # with the final integral being:
265            #    \int\int I(q, theta, phi) sin(R)/R dtheta dphi
266            #
267            # This does approximately the right thing, decreasing the weight
268            # of each point as you go farther out on the disk, but it hasn't
269            # yet been checked against the 1D integral results. Prior
270            # to declaring this "good enough" and checking that integrals
271            # work in practice, we will examine alternative mappings.
272            #
273            # The issue is that the mapping does not support the case of free
274            # rotation about a single axis correctly, with a small deviation
275            # in the orthogonal axis independent of the first axis.  Like the
276            # usual polar coordiates integration, the integrated sections
277            # form wedges, though at least in this case the wedge cuts through
278            # the entire sphere, and treats theta and phi identically.
279            latitude = sqrt(theta_i**2 + phi_j**2)
280            w = sin(radians(latitude))/latitude if latitude != 0 else 1
281            return w*wi*wj if latitude < 180 else 0
282    elif projection == 'sinusoidal':
283        def rotate(theta_i, phi_j):
284            latitude = theta_i
285            scale = cos(radians(latitude))
286            longitude = phi_j/scale if abs(phi_j) < abs(scale)*180 else 0
287            #print("(%+7.2f, %+7.2f) => (%+7.2f, %+7.2f)"%(theta_i, phi_j, latitude, longitude))
288            return Rx(longitude)*Ry(latitude)
289        def weight(theta_i, phi_j, wi, wj):
290            latitude = theta_i
291            scale = cos(radians(latitude))
292            w = 1 if abs(phi_j) < abs(scale)*180 else 0
293            return w*wi*wj
294    elif projection == 'azimuthal_equal_area':
295        def rotate(theta_i, phi_j):
296            R = min(1, sqrt(theta_i**2 + phi_j**2)/180)
297            latitude = 180-degrees(2*np.arccos(R))
298            longitude = degrees(np.arctan2(phi_j, theta_i))
299            #print("(%+7.2f, %+7.2f) => (%+7.2f, %+7.2f)"%(theta_i, phi_j, latitude, longitude))
300            return Rz(longitude)*Ry(latitude)
301        def weight(theta_i, phi_j, wi, wj):
302            latitude = sqrt(theta_i**2 + phi_j**2)
303            w = sin(radians(latitude))/latitude if latitude != 0 else 1
304            return w*wi*wj if latitude < 180 else 0
305    else:
306        raise ValueError("unknown projection %r"%projection)
307
308    # mesh in theta, phi formed by rotating z
309    z = np.matrix([[0], [0], [radius]])
310    points = np.hstack([rotate(theta_i, phi_j)*z
311                        for theta_i in dtheta*t
312                        for phi_j in dphi*t])
313    # select just the active points (i.e., those with phi < 180
314    w = np.array([weight(theta_i, phi_j, wi, wj)
315                  for wi, theta_i in zip(weights, dtheta*t)
316                  for wj, phi_j in zip(weights, dphi*t)])
317    #print(max(w), min(w), min(w[w>0]))
318    points = points[:, w>0]
319    w = w[w>0]
320    w /= max(w)
321
322    if 0: # Kent distribution
323        points = np.hstack([Rx(phi_j)*Ry(theta_i)*z for theta_i in 30*t for phi_j in 60*t])
324        xp, yp, zp = [np.array(v).flatten() for v in points]
325        kappa = max(1e6, radians(dtheta)/(2*pi))
326        beta = 1/max(1e-6, radians(dphi)/(2*pi))/kappa
327        w = exp(kappa*zp) #+ beta*(xp**2 + yp**2)
328        print(kappa, dtheta, radians(dtheta), min(w), max(w), sum(w))
329        #w /= abs(cos(radians(
330        #w /= sum(w)
331
332    # rotate relative to beam
333    points = orient_relative_to_beam(view, points)
334
335    x, y, z = [np.array(v).flatten() for v in points]
336    #plt.figure(2); plt.clf(); plt.hist(z, bins=np.linspace(-1, 1, 51))
337    ax.scatter(x, y, z, c=w, marker='o', vmin=0., vmax=1.)
338
339def draw_labels(ax, view, jitter, text):
340    """
341    Draw text at a particular location.
342    """
343    labels, locations, orientations = zip(*text)
344    px, py, pz = zip(*locations)
345    dx, dy, dz = zip(*orientations)
346
347    px, py, pz = transform_xyz(view, jitter, px, py, pz)
348    dx, dy, dz = transform_xyz(view, jitter, dx, dy, dz)
349
350    # TODO: zdir for labels is broken, and labels aren't appearing.
351    for label, p, zdir in zip(labels, zip(px, py, pz), zip(dx, dy, dz)):
352        zdir = np.asarray(zdir).flatten()
353        ax.text(p[0], p[1], p[2], label, zdir=zdir)
354
355# Definition of rotation matrices comes from wikipedia:
356#    https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations
357def Rx(angle):
358    """Construct a matrix to rotate points about *x* by *angle* degrees."""
359    a = radians(angle)
360    R = [[1, 0, 0],
361         [0, +cos(a), -sin(a)],
362         [0, +sin(a), +cos(a)]]
363    return np.matrix(R)
364
365def Ry(angle):
366    """Construct a matrix to rotate points about *y* by *angle* degrees."""
367    a = radians(angle)
368    R = [[+cos(a), 0, +sin(a)],
369         [0, 1, 0],
370         [-sin(a), 0, +cos(a)]]
371    return np.matrix(R)
372
373def Rz(angle):
374    """Construct a matrix to rotate points about *z* by *angle* degrees."""
375    a = radians(angle)
376    R = [[+cos(a), -sin(a), 0],
377         [+sin(a), +cos(a), 0],
378         [0, 0, 1]]
379    return np.matrix(R)
380
381def transform_xyz(view, jitter, x, y, z):
382    """
383    Send a set of (x,y,z) points through the jitter and view transforms.
384    """
385    x, y, z = [np.asarray(v) for v in (x, y, z)]
386    shape = x.shape
387    points = np.matrix([x.flatten(),y.flatten(),z.flatten()])
388    points = apply_jitter(jitter, points)
389    points = orient_relative_to_beam(view, points)
390    x, y, z = [np.array(v).reshape(shape) for v in points]
391    return x, y, z
392
393def apply_jitter(jitter, points):
394    """
395    Apply the jitter transform to a set of points.
396
397    Points are stored in a 3 x n numpy matrix, not a numpy array or tuple.
398    """
399    dtheta, dphi, dpsi = jitter
400    points = Rx(dphi)*Ry(dtheta)*Rz(dpsi)*points
401    return points
402
403def orient_relative_to_beam(view, points):
404    """
405    Apply the view 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    theta, phi, psi = view
410    points = Rz(phi)*Ry(theta)*Rz(psi)*points
411    return points
412
413# translate between number of dimension of dispersity and the number of
414# points along each dimension.
415PD_N_TABLE = {
416    (0, 0, 0): (0, 0, 0),     # 0
417    (1, 0, 0): (100, 0, 0),   # 100
418    (0, 1, 0): (0, 100, 0),
419    (0, 0, 1): (0, 0, 100),
420    (1, 1, 0): (30, 30, 0),   # 900
421    (1, 0, 1): (30, 0, 30),
422    (0, 1, 1): (0, 30, 30),
423    (1, 1, 1): (15, 15, 15),  # 3375
424}
425
426def clipped_range(data, portion=1.0, mode='central'):
427    """
428    Determine range from data.
429
430    If *portion* is 1, use full range, otherwise use the center of the range
431    or the top of the range, depending on whether *mode* is 'central' or 'top'.
432    """
433    if portion == 1.0:
434        return data.min(), data.max()
435    elif mode == 'central':
436        data = np.sort(data.flatten())
437        offset = int(portion*len(data)/2 + 0.5)
438        return data[offset], data[-offset]
439    elif mode == 'top':
440        data = np.sort(data.flatten())
441        offset = int(portion*len(data) + 0.5)
442        return data[offset], data[-1]
443
444def draw_scattering(calculator, ax, view, jitter, dist='gaussian'):
445    """
446    Plot the scattering for the particular view.
447
448    *calculator* is returned from :func:`build_model`.  *ax* are the 3D axes
449    on which the data will be plotted.  *view* and *jitter* are the current
450    orientation and orientation dispersity.  *dist* is one of the sasmodels
451    weight distributions.
452    """
453    if dist == 'uniform':  # uniform is not yet in this branch
454        dist, scale = 'rectangle', 1/sqrt(3)
455    else:
456        scale = 1
457
458    # add the orientation parameters to the model parameters
459    theta, phi, psi = view
460    theta_pd, phi_pd, psi_pd = [scale*v for v in jitter]
461    theta_pd_n, phi_pd_n, psi_pd_n = PD_N_TABLE[(theta_pd>0, phi_pd>0, psi_pd>0)]
462    ## increase pd_n for testing jitter integration rather than simple viz
463    #theta_pd_n, phi_pd_n, psi_pd_n = [5*v for v in (theta_pd_n, phi_pd_n, psi_pd_n)]
464
465    pars = dict(
466        theta=theta, theta_pd=theta_pd, theta_pd_type=dist, theta_pd_n=theta_pd_n,
467        phi=phi, phi_pd=phi_pd, phi_pd_type=dist, phi_pd_n=phi_pd_n,
468        psi=psi, psi_pd=psi_pd, psi_pd_type=dist, psi_pd_n=psi_pd_n,
469    )
470    pars.update(calculator.pars)
471
472    # compute the pattern
473    qx, qy = calculator._data.x_bins, calculator._data.y_bins
474    Iqxy = calculator(**pars).reshape(len(qx), len(qy))
475
476    # scale it and draw it
477    Iqxy = np.log(Iqxy)
478    if calculator.limits:
479        # use limits from orientation (0,0,0)
480        vmin, vmax = calculator.limits
481    else:
482        vmin, vmax = clipped_range(Iqxy, portion=0.95, mode='top')
483    #print("range",(vmin,vmax))
484    #qx, qy = np.meshgrid(qx, qy)
485    if 0:
486        level = np.asarray(255*(Iqxy - vmin)/(vmax - vmin), 'i')
487        level[level<0] = 0
488        colors = plt.get_cmap()(level)
489        ax.plot_surface(qx, qy, -1.1, rstride=1, cstride=1, facecolors=colors)
490    elif 1:
491        ax.contourf(qx/qx.max(), qy/qy.max(), Iqxy, zdir='z', offset=-1.1,
492                    levels=np.linspace(vmin, vmax, 24))
493    else:
494        ax.pcolormesh(qx, qy, Iqxy)
495
496def build_model(model_name, n=150, qmax=0.5, **pars):
497    """
498    Build a calculator for the given shape.
499
500    *model_name* is any sasmodels model.  *n* and *qmax* define an n x n mesh
501    on which to evaluate the model.  The remaining parameters are stored in
502    the returned calculator as *calculator.pars*.  They are used by
503    :func:`draw_scattering` to set the non-orientation parameters in the
504    calculation.
505
506    Returns a *calculator* function which takes a dictionary or parameters and
507    produces Iqxy.  The Iqxy value needs to be reshaped to an n x n matrix
508    for plotting.  See the :class:`sasmodels.direct_model.DirectModel` class
509    for details.
510    """
511    from sasmodels.core import load_model_info, build_model
512    from sasmodels.data import empty_data2D
513    from sasmodels.direct_model import DirectModel
514
515    model_info = load_model_info(model_name)
516    model = build_model(model_info) #, dtype='double!')
517    q = np.linspace(-qmax, qmax, n)
518    data = empty_data2D(q, q)
519    calculator = DirectModel(data, model)
520
521    # stuff the values for non-orientation parameters into the calculator
522    calculator.pars = pars.copy()
523    calculator.pars.setdefault('backgound', 1e-3)
524
525    # fix the data limits so that we can see if the pattern fades
526    # under rotation or angular dispersion
527    Iqxy = calculator(theta=0, phi=0, psi=0, **calculator.pars)
528    Iqxy = np.log(Iqxy)
529    vmin, vmax = clipped_range(Iqxy, 0.95, mode='top')
530    calculator.limits = vmin, vmax+1
531
532    return calculator
533
534def select_calculator(model_name, n=150, size=(10,40,100)):
535    """
536    Create a model calculator for the given shape.
537
538    *model_name* is one of sphere, cylinder, ellipsoid, triaxial_ellipsoid,
539    parallelepiped or bcc_paracrystal. *n* is the number of points to use
540    in the q range.  *qmax* is chosen based on model parameters for the
541    given model to show something intersting.
542
543    Returns *calculator* and tuple *size* (a,b,c) giving minor and major
544    equitorial axes and polar axis respectively.  See :func:`build_model`
545    for details on the returned calculator.
546    """
547    a, b, c = size
548    if model_name == 'sphere':
549        calculator = build_model('sphere', n=n, radius=c)
550        a = b = c
551    elif model_name == 'bcc_paracrystal':
552        calculator = build_model('bcc_paracrystal', n=n, dnn=c,
553                                  d_factor=0.06, radius=40)
554        a = b = c
555    elif model_name == 'cylinder':
556        calculator = build_model('cylinder', n=n, qmax=0.3, radius=b, length=c)
557        a = b
558    elif model_name == 'ellipsoid':
559        calculator = build_model('ellipsoid', n=n, qmax=1.0,
560                                 radius_polar=c, radius_equatorial=b)
561        a = b
562    elif model_name == 'triaxial_ellipsoid':
563        calculator = build_model('triaxial_ellipsoid', n=n, qmax=0.5,
564                                 radius_equat_minor=a,
565                                 radius_equat_major=b,
566                                 radius_polar=c)
567    elif model_name == 'parallelepiped':
568        calculator = build_model('parallelepiped', n=n, a=a, b=b, c=c)
569    else:
570        raise ValueError("unknown model %s"%model_name)
571
572    return calculator, (a, b, c)
573
574SHAPES = [
575    'parallelepiped', 'triaxial_ellipsoid', 'bcc_paracrystal',
576    'cylinder', 'ellipsoid',
577    'sphere',
578 ]
579
580DISTRIBUTIONS = [
581    'gaussian', 'rectangle', 'uniform',
582]
583DIST_LIMITS = {
584    'gaussian': 30,
585    'rectangle': 90/sqrt(3),
586    'uniform': 90,
587}
588
589def run(model_name='parallelepiped', size=(10, 40, 100),
590        dist='gaussian', mesh=30,
591        projection='equirectangular'):
592    """
593    Show an interactive orientation and jitter demo.
594
595    *model_name* is one of the models available in :func:`select_model`.
596    """
597    # set up calculator
598    calculator, size = select_calculator(model_name, n=150, size=size)
599
600    ## uncomment to set an independent the colour range for every view
601    ## If left commented, the colour range is fixed for all views
602    calculator.limits = None
603
604    ## initial view
605    #theta, dtheta = 70., 10.
606    #phi, dphi = -45., 3.
607    #psi, dpsi = -45., 3.
608    theta, phi, psi = 0, 0, 0
609    dtheta, dphi, dpsi = 0, 0, 0
610
611    ## create the plot window
612    #plt.hold(True)
613    plt.set_cmap('gist_earth')
614    plt.clf()
615    #gs = gridspec.GridSpec(2,1,height_ratios=[4,1])
616    #ax = plt.subplot(gs[0], projection='3d')
617    ax = plt.axes([0.0, 0.2, 1.0, 0.8], projection='3d')
618    try:  # CRUFT: not all versions of matplotlib accept 'square' 3d projection
619        ax.axis('square')
620    except Exception:
621        pass
622
623    axcolor = 'lightgoldenrodyellow'
624
625    ## add control widgets to plot
626    axtheta  = plt.axes([0.1, 0.15, 0.45, 0.04], axisbg=axcolor)
627    axphi = plt.axes([0.1, 0.1, 0.45, 0.04], axisbg=axcolor)
628    axpsi = plt.axes([0.1, 0.05, 0.45, 0.04], axisbg=axcolor)
629    stheta = Slider(axtheta, 'Theta', -90, 90, valinit=theta)
630    sphi = Slider(axphi, 'Phi', -180, 180, valinit=phi)
631    spsi = Slider(axpsi, 'Psi', -180, 180, valinit=psi)
632
633    axdtheta  = plt.axes([0.75, 0.15, 0.15, 0.04], axisbg=axcolor)
634    axdphi = plt.axes([0.75, 0.1, 0.15, 0.04], axisbg=axcolor)
635    axdpsi= plt.axes([0.75, 0.05, 0.15, 0.04], axisbg=axcolor)
636    # Note: using ridiculous definition of rectangle distribution, whose width
637    # in sasmodels is sqrt(3) times the given width.  Divide by sqrt(3) to keep
638    # the maximum width to 90.
639    dlimit = DIST_LIMITS[dist]
640    sdtheta = Slider(axdtheta, 'dTheta', 0, 2*dlimit, valinit=dtheta)
641    sdphi = Slider(axdphi, 'dPhi', 0, 2*dlimit, valinit=dphi)
642    sdpsi = Slider(axdpsi, 'dPsi', 0, 2*dlimit, valinit=dpsi)
643
644    ## callback to draw the new view
645    def update(val, axis=None):
646        view = stheta.val, sphi.val, spsi.val
647        jitter = sdtheta.val, sdphi.val, sdpsi.val
648        # set small jitter as 0 if multiple pd dims
649        dims = sum(v > 0 for v in jitter)
650        limit = [0, 0, 0.5, 5][dims]
651        jitter = [0 if v < limit else v for v in jitter]
652        ax.cla()
653        draw_beam(ax, (0, 0))
654        draw_jitter(ax, view, jitter, dist=dist, size=size)
655        #draw_jitter(ax, view, (0,0,0))
656        draw_mesh(ax, view, jitter, dist=dist, n=mesh, projection=projection)
657        draw_scattering(calculator, ax, view, jitter, dist=dist)
658        plt.gcf().canvas.draw()
659
660    ## bind control widgets to view updater
661    stheta.on_changed(lambda v: update(v,'theta'))
662    sphi.on_changed(lambda v: update(v, 'phi'))
663    spsi.on_changed(lambda v: update(v, 'psi'))
664    sdtheta.on_changed(lambda v: update(v, 'dtheta'))
665    sdphi.on_changed(lambda v: update(v, 'dphi'))
666    sdpsi.on_changed(lambda v: update(v, 'dpsi'))
667
668    ## initialize view
669    update(None, 'phi')
670
671    ## go interactive
672    plt.show()
673
674def main():
675    parser = argparse.ArgumentParser(
676        description="Display jitter",
677        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
678        )
679    parser.add_argument('-p', '--projection', choices=PROJECTIONS, default=PROJECTIONS[0], help='coordinate projection')
680    parser.add_argument('-s', '--size', type=str, default='10,40,100', help='a,b,c lengths')
681    parser.add_argument('-d', '--distribution', choices=DISTRIBUTIONS, default=DISTRIBUTIONS[0], help='jitter distribution')
682    parser.add_argument('-m', '--mesh', type=int, default=30, help='#points in theta-phi mesh')
683    parser.add_argument('shape', choices=SHAPES, nargs='?', default=SHAPES[0], help='oriented shape')
684    opts = parser.parse_args()
685    size = tuple(int(v) for v in opts.size.split(','))
686    run(opts.shape, size=size,
687        mesh=opts.mesh, dist=opts.distribution,
688        projection=opts.projection)
689
690if __name__ == "__main__":
691    main()
Note: See TracBrowser for help on using the repository browser.