source: sasmodels/explore/jitter.py @ 31a02f1

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

draw one face of parallelepiped in different colour on jitter visualization

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