#!/usr/bin/env python
"""
Jitter Explorer
===============
Application to explore orientation angle and angular dispersity.
"""
from __future__ import division, print_function
import argparse
try: # CRUFT: travis-ci does not support mpl_toolkits.mplot3d
import mpl_toolkits.mplot3d # Adds projection='3d' option to subplot
except ImportError:
pass
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
import numpy as np
from numpy import pi, cos, sin, sqrt, exp, degrees, radians
[docs]def draw_beam(axes, view=(0, 0)):
"""
Draw the beam going from source at (0, 0, 1) to detector at (0, 0, -1)
"""
#axes.plot([0,0],[0,0],[1,-1])
#axes.scatter([0]*100,[0]*100,np.linspace(1, -1, 100), alpha=0.8)
steps = 25
u = np.linspace(0, 2 * np.pi, steps)
v = np.linspace(-1, 1, steps)
r = 0.02
x = r*np.outer(np.cos(u), np.ones_like(v))
y = r*np.outer(np.sin(u), np.ones_like(v))
z = 1.3*np.outer(np.ones_like(u), v)
theta, phi = view
shape = x.shape
points = np.matrix([x.flatten(), y.flatten(), z.flatten()])
points = Rz(phi)*Ry(theta)*points
x, y, z = [v.reshape(shape) for v in points]
axes.plot_surface(x, y, z, rstride=4, cstride=4, color='y', alpha=0.5)
[docs]def draw_ellipsoid(axes, size, view, jitter, steps=25, alpha=1):
"""Draw an ellipsoid."""
a, b, c = size
u = np.linspace(0, 2 * np.pi, steps)
v = np.linspace(0, np.pi, steps)
x = a*np.outer(np.cos(u), np.sin(v))
y = b*np.outer(np.sin(u), np.sin(v))
z = c*np.outer(np.ones_like(u), np.cos(v))
x, y, z = transform_xyz(view, jitter, x, y, z)
axes.plot_surface(x, y, z, rstride=4, cstride=4, color='w', alpha=alpha)
draw_labels(axes, view, jitter, [
('c+', [+0, +0, +c], [+1, +0, +0]),
('c-', [+0, +0, -c], [+0, +0, -1]),
('a+', [+a, +0, +0], [+0, +0, +1]),
('a-', [-a, +0, +0], [+0, +0, -1]),
('b+', [+0, +b, +0], [-1, +0, +0]),
('b-', [+0, -b, +0], [-1, +0, +0]),
])
[docs]def draw_sc(axes, size, view, jitter, steps=None, alpha=1):
"""Draw points for simple cubic paracrystal"""
atoms = _build_sc()
_draw_crystal(axes, size, view, jitter, atoms=atoms)
[docs]def draw_fcc(axes, size, view, jitter, steps=None, alpha=1):
"""Draw points for face-centered cubic paracrystal"""
# Build the simple cubic crystal
atoms = _build_sc()
# Define the centers for each face
# x planes at -1, 0, 1 have four centers per plane, at +/- 0.5 in y and z
x, y, z = (
[-1]*4 + [0]*4 + [1]*4,
([-0.5]*2 + [0.5]*2)*3,
[-0.5, 0.5]*12,
)
# y and z planes can be generated by substituting x for y and z respectively
atoms.extend(zip(x+y+z, y+z+x, z+x+y))
_draw_crystal(axes, size, view, jitter, atoms=atoms)
[docs]def draw_bcc(axes, size, view, jitter, steps=None, alpha=1):
"""Draw points for body-centered cubic paracrystal"""
# Build the simple cubic crystal
atoms = _build_sc()
# Define the centers for each octant
# x plane at +/- 0.5 have four centers per plane at +/- 0.5 in y and z
x, y, z = (
[-0.5]*4 + [0.5]*4,
([-0.5]*2 + [0.5]*2)*2,
[-0.5, 0.5]*8,
)
atoms.extend(zip(x, y, z))
_draw_crystal(axes, size, view, jitter, atoms=atoms)
def _draw_crystal(axes, size, view, jitter, atoms=None):
atoms, size = np.asarray(atoms, 'd').T, np.asarray(size, 'd')
x, y, z = atoms*size[:, None]
x, y, z = transform_xyz(view, jitter, x, y, z)
axes.scatter([x[0]], [y[0]], [z[0]], c='yellow', marker='o')
axes.scatter(x[1:], y[1:], z[1:], c='r', marker='o')
def _build_sc():
# three planes of 9 dots for x at -1, 0 and 1
x, y, z = (
[-1]*9 + [0]*9 + [1]*9,
([-1]*3 + [0]*3 + [1]*3)*3,
[-1, 0, 1]*9,
)
atoms = list(zip(x, y, z))
#print(list(enumerate(atoms)))
# Pull the dot at (0, 0, 1) to the front of the list
# It will be highlighted in the view
index = 14
highlight = atoms[index]
del atoms[index]
atoms.insert(0, highlight)
return atoms
[docs]def draw_parallelepiped(axes, size, view, jitter, steps=None, alpha=1):
"""Draw a parallelepiped."""
a, b, c = size
x = a*np.array([+1, -1, +1, -1, +1, -1, +1, -1])
y = b*np.array([+1, +1, -1, -1, +1, +1, -1, -1])
z = c*np.array([+1, +1, +1, +1, -1, -1, -1, -1])
tri = np.array([
# counter clockwise triangles
# z: up/down, x: right/left, y: front/back
[0, 1, 2], [3, 2, 1], # top face
[6, 5, 4], [5, 6, 7], # bottom face
[0, 2, 6], [6, 4, 0], # right face
[1, 5, 7], [7, 3, 1], # left face
[2, 3, 6], [7, 6, 3], # front face
[4, 1, 0], [5, 1, 4], # back face
])
x, y, z = transform_xyz(view, jitter, x, y, z)
axes.plot_trisurf(x, y, triangles=tri, Z=z, color='w', alpha=alpha)
# Draw pink face on box.
# Since I can't control face color, instead draw a thin box situated just
# in front of the "c+" face. Use the c face so that rotations about psi
# rotate that face.
if 1:
x = a*np.array([+1, -1, +1, -1, +1, -1, +1, -1])
y = b*np.array([+1, +1, -1, -1, +1, +1, -1, -1])
z = c*np.array([+1, +1, +1, +1, -1, -1, -1, -1])
x, y, z = transform_xyz(view, jitter, x, y, abs(z)+0.001)
axes.plot_trisurf(x, y, triangles=tri, Z=z, color=[1, 0.6, 0.6], alpha=alpha)
draw_labels(axes, view, jitter, [
('c+', [+0, +0, +c], [+1, +0, +0]),
('c-', [+0, +0, -c], [+0, +0, -1]),
('a+', [+a, +0, +0], [+0, +0, +1]),
('a-', [-a, +0, +0], [+0, +0, -1]),
('b+', [+0, +b, +0], [-1, +0, +0]),
('b-', [+0, -b, +0], [-1, +0, +0]),
])
[docs]def draw_sphere(axes, radius=10., steps=100):
"""Draw a sphere"""
u = np.linspace(0, 2 * np.pi, steps)
v = np.linspace(0, np.pi, steps)
x = radius * np.outer(np.cos(u), np.sin(v))
y = radius * np.outer(np.sin(u), np.sin(v))
z = radius * np.outer(np.ones(np.size(u)), np.cos(v))
axes.plot_surface(x, y, z, rstride=4, cstride=4, color='w')
[docs]def draw_jitter(axes, view, jitter, dist='gaussian', size=(0.1, 0.4, 1.0),
draw_shape=draw_parallelepiped):
"""
Represent jitter as a set of shapes at different orientations.
"""
# set max diagonal to 0.95
scale = 0.95/sqrt(sum(v**2 for v in size))
size = tuple(scale*v for v in size)
#np.random.seed(10)
#cloud = np.random.randn(10,3)
cloud = [
[-1, -1, -1],
[-1, -1, +0],
[-1, -1, +1],
[-1, +0, -1],
[-1, +0, +0],
[-1, +0, +1],
[-1, +1, -1],
[-1, +1, +0],
[-1, +1, +1],
[+0, -1, -1],
[+0, -1, +0],
[+0, -1, +1],
[+0, +0, -1],
[+0, +0, +0],
[+0, +0, +1],
[+0, +1, -1],
[+0, +1, +0],
[+0, +1, +1],
[+1, -1, -1],
[+1, -1, +0],
[+1, -1, +1],
[+1, +0, -1],
[+1, +0, +0],
[+1, +0, +1],
[+1, +1, -1],
[+1, +1, +0],
[+1, +1, +1],
]
dtheta, dphi, dpsi = jitter
if dtheta == 0:
cloud = [v for v in cloud if v[0] == 0]
if dphi == 0:
cloud = [v for v in cloud if v[1] == 0]
if dpsi == 0:
cloud = [v for v in cloud if v[2] == 0]
draw_shape(axes, size, view, [0, 0, 0], steps=100, alpha=0.8)
scale = {'gaussian':1, 'rectangle':1/sqrt(3), 'uniform':1/3}[dist]
for point in cloud:
delta = [scale*dtheta*point[0], scale*dphi*point[1], scale*dpsi*point[2]]
draw_shape(axes, size, view, delta, alpha=0.8)
for v in 'xyz':
a, b, c = size
lim = np.sqrt(a**2 + b**2 + c**2)
getattr(axes, 'set_'+v+'lim')([-lim, lim])
getattr(axes, v+'axis').label.set_text(v)
PROJECTIONS = [
# in order of PROJECTION number; do not change without updating the
# constants in kernel_iq.c
'equirectangular', 'sinusoidal', 'guyou', 'azimuthal_equidistance',
'azimuthal_equal_area',
]
[docs]def draw_mesh(axes, view, jitter, radius=1.2, n=11, dist='gaussian',
projection='equirectangular'):
"""
Draw the dispersion mesh showing the theta-phi orientations at which
the model will be evaluated.
jitter projections
<https://en.wikipedia.org/wiki/List_of_map_projections>
equirectangular (standard latitude-longitude mesh)
<https://en.wikipedia.org/wiki/Equirectangular_projection>
Allows free movement in phi (around the equator), but theta is
limited to +/- 90, and points are cos-weighted. Jitter in phi is
uniform in weight along a line of latitude. With small theta and
phi ranging over +/- 180 this forms a wobbling disk. With small
phi and theta ranging over +/- 90 this forms a wedge like a slice
of an orange.
azimuthal_equidistance (Postel)
<https://en.wikipedia.org/wiki/Azimuthal_equidistant_projection>
Preserves distance from center, and so is an excellent map for
representing a bivariate gaussian on the surface. Theta and phi
operate identically, cutting wegdes from the antipode of the viewing
angle. This unfortunately does not allow free movement in either
theta or phi since the orthogonal wobble decreases to 0 as the body
rotates through 180 degrees.
sinusoidal (Sanson-Flamsteed, Mercator equal-area)
<https://en.wikipedia.org/wiki/Sinusoidal_projection>
Preserves arc length with latitude, giving bad behaviour at
theta near +/- 90. Theta and phi operate somewhat differently,
so a system with a-b-c dtheta-dphi-dpsi will not give the same
value as one with b-a-c dphi-dtheta-dpsi, as would be the case
for azimuthal equidistance. Free movement using theta or phi
uniform over +/- 180 will work, but not as well as equirectangular
phi, with theta being slightly worse. Computationally it is much
cheaper for wide theta-phi meshes since it excludes points which
lie outside the sinusoid near theta +/- 90 rather than packing
them close together as in equirectangle. Note that the poles
will be slightly overweighted for theta > 90 with the circle
from theta at 90+dt winding backwards around the pole, overlapping
the circle from theta at 90-dt.
Guyou (hemisphere-in-a-square) **not weighted**
<https://en.wikipedia.org/wiki/Guyou_hemisphere-in-a-square_projection>
With tiling, allows rotation in phi or theta through +/- 180, with
uniform spacing. Both theta and phi allow free rotation, with wobble
in the orthogonal direction reasonably well behaved (though not as
good as equirectangular phi). The forward/reverse transformations
relies on elliptic integrals that are somewhat expensive, so the
behaviour has to be very good to justify the cost and complexity.
The weighting function for each point has not yet been computed.
Note: run the module *guyou.py* directly and it will show the forward
and reverse mappings.
azimuthal_equal_area **incomplete**
<https://en.wikipedia.org/wiki/Lambert_azimuthal_equal-area_projection>
Preserves the relative density of the surface patches. Not that
useful and not completely implemented
Gauss-Kreuger **not implemented**
<https://en.wikipedia.org/wiki/Transverse_Mercator_projection#Ellipsoidal_transverse_Mercator>
Should allow free movement in theta, but phi is distorted.
"""
# TODO: try Kent distribution instead of a gaussian warped by projection
dist_x = np.linspace(-1, 1, n)
weights = np.ones_like(dist_x)
if dist == 'gaussian':
dist_x *= 3
weights = exp(-0.5*dist_x**2)
elif dist == 'rectangle':
# Note: uses sasmodels ridiculous definition of rectangle width
dist_x *= sqrt(3)
elif dist == 'uniform':
pass
else:
raise ValueError("expected dist to be gaussian, rectangle or uniform")
if projection == 'equirectangular': #define PROJECTION 1
def _rotate(theta_i, phi_j):
return Rx(phi_j)*Ry(theta_i)
def _weight(theta_i, phi_j, w_i, w_j):
return w_i*w_j*abs(cos(radians(theta_i)))
elif projection == 'sinusoidal': #define PROJECTION 2
def _rotate(theta_i, phi_j):
latitude = theta_i
scale = cos(radians(latitude))
longitude = phi_j/scale if abs(phi_j) < abs(scale)*180 else 0
#print("(%+7.2f, %+7.2f) => (%+7.2f, %+7.2f)"%(theta_i, phi_j, latitude, longitude))
return Rx(longitude)*Ry(latitude)
def _weight(theta_i, phi_j, w_i, w_j):
latitude = theta_i
scale = cos(radians(latitude))
active = 1 if abs(phi_j) < abs(scale)*180 else 0
return active*w_i*w_j
elif projection == 'guyou': #define PROJECTION 3 (eventually?)
def _rotate(theta_i, phi_j):
from .guyou import guyou_invert
#latitude, longitude = guyou_invert([theta_i], [phi_j])
longitude, latitude = guyou_invert([phi_j], [theta_i])
return Rx(longitude[0])*Ry(latitude[0])
def _weight(theta_i, phi_j, w_i, w_j):
return w_i*w_j
elif projection == 'azimuthal_equidistance': # Note: Rz Ry, not Rx Ry
def _rotate(theta_i, phi_j):
latitude = sqrt(theta_i**2 + phi_j**2)
longitude = degrees(np.arctan2(phi_j, theta_i))
#print("(%+7.2f, %+7.2f) => (%+7.2f, %+7.2f)"%(theta_i, phi_j, latitude, longitude))
return Rz(longitude)*Ry(latitude)
def _weight(theta_i, phi_j, w_i, w_j):
# Weighting for each point comes from the integral:
# \int\int I(q, lat, log) sin(lat) dlat dlog
# We are doing a conformal mapping from disk to sphere, so we need
# a change of variables g(theta, phi) -> (lat, long):
# lat, long = sqrt(theta^2 + phi^2), arctan(phi/theta)
# giving:
# dtheta dphi = det(J) dlat dlong
# where J is the jacobian from the partials of g. Using
# R = sqrt(theta^2 + phi^2),
# then
# J = [[x/R, Y/R], -y/R^2, x/R^2]]
# and
# det(J) = 1/R
# with the final integral being:
# \int\int I(q, theta, phi) sin(R)/R dtheta dphi
#
# This does approximately the right thing, decreasing the weight
# of each point as you go farther out on the disk, but it hasn't
# yet been checked against the 1D integral results. Prior
# to declaring this "good enough" and checking that integrals
# work in practice, we will examine alternative mappings.
#
# The issue is that the mapping does not support the case of free
# rotation about a single axis correctly, with a small deviation
# in the orthogonal axis independent of the first axis. Like the
# usual polar coordiates integration, the integrated sections
# form wedges, though at least in this case the wedge cuts through
# the entire sphere, and treats theta and phi identically.
latitude = sqrt(theta_i**2 + phi_j**2)
weight = sin(radians(latitude))/latitude if latitude != 0 else 1
return weight*w_i*w_j if latitude < 180 else 0
elif projection == 'azimuthal_equal_area':
def _rotate(theta_i, phi_j):
radius = min(1, sqrt(theta_i**2 + phi_j**2)/180)
latitude = 180-degrees(2*np.arccos(radius))
longitude = degrees(np.arctan2(phi_j, theta_i))
#print("(%+7.2f, %+7.2f) => (%+7.2f, %+7.2f)"%(theta_i, phi_j, latitude, longitude))
return Rz(longitude)*Ry(latitude)
def _weight(theta_i, phi_j, w_i, w_j):
latitude = sqrt(theta_i**2 + phi_j**2)
weight = sin(radians(latitude))/latitude if latitude != 0 else 1
return weight*w_i*w_j if latitude < 180 else 0
else:
raise ValueError("unknown projection %r"%projection)
# mesh in theta, phi formed by rotating z
dtheta, dphi, dpsi = jitter
z = np.matrix([[0], [0], [radius]])
points = np.hstack([_rotate(theta_i, phi_j)*z
for theta_i in dtheta*dist_x
for phi_j in dphi*dist_x])
dist_w = np.array([_weight(theta_i, phi_j, w_i, w_j)
for w_i, theta_i in zip(weights, dtheta*dist_x)
for w_j, phi_j in zip(weights, dphi*dist_x)])
#print(max(dist_w), min(dist_w), min(dist_w[dist_w > 0]))
points = points[:, dist_w > 0]
dist_w = dist_w[dist_w > 0]
dist_w /= max(dist_w)
# rotate relative to beam
points = orient_relative_to_beam(view, points)
x, y, z = [np.array(v).flatten() for v in points]
#plt.figure(2); plt.clf(); plt.hist(z, bins=np.linspace(-1, 1, 51))
axes.scatter(x, y, z, c=dist_w, marker='o', vmin=0., vmax=1.)
[docs]def draw_labels(axes, view, jitter, text):
"""
Draw text at a particular location.
"""
labels, locations, orientations = zip(*text)
px, py, pz = zip(*locations)
dx, dy, dz = zip(*orientations)
px, py, pz = transform_xyz(view, jitter, px, py, pz)
dx, dy, dz = transform_xyz(view, jitter, dx, dy, dz)
# TODO: zdir for labels is broken, and labels aren't appearing.
for label, p, zdir in zip(labels, zip(px, py, pz), zip(dx, dy, dz)):
zdir = np.asarray(zdir).flatten()
axes.text(p[0], p[1], p[2], label, zdir=zdir)
# Definition of rotation matrices comes from wikipedia:
# https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations
[docs]def Rx(angle):
"""Construct a matrix to rotate points about *x* by *angle* degrees."""
angle = radians(angle)
rot = [[1, 0, 0],
[0, +cos(angle), -sin(angle)],
[0, +sin(angle), +cos(angle)]]
return np.matrix(rot)
[docs]def Ry(angle):
"""Construct a matrix to rotate points about *y* by *angle* degrees."""
angle = radians(angle)
rot = [[+cos(angle), 0, +sin(angle)],
[0, 1, 0],
[-sin(angle), 0, +cos(angle)]]
return np.matrix(rot)
[docs]def Rz(angle):
"""Construct a matrix to rotate points about *z* by *angle* degrees."""
angle = radians(angle)
rot = [[+cos(angle), -sin(angle), 0],
[+sin(angle), +cos(angle), 0],
[0, 0, 1]]
return np.matrix(rot)
[docs]def apply_jitter(jitter, points):
"""
Apply the jitter transform to a set of points.
Points are stored in a 3 x n numpy matrix, not a numpy array or tuple.
"""
dtheta, dphi, dpsi = jitter
points = Rx(dphi)*Ry(dtheta)*Rz(dpsi)*points
return points
[docs]def orient_relative_to_beam(view, points):
"""
Apply the view transform to a set of points.
Points are stored in a 3 x n numpy matrix, not a numpy array or tuple.
"""
theta, phi, psi = view
points = Rz(phi)*Ry(theta)*Rz(psi)*points
return points
# translate between number of dimension of dispersity and the number of
# points along each dimension.
PD_N_TABLE = {
(0, 0, 0): (0, 0, 0), # 0
(1, 0, 0): (100, 0, 0), # 100
(0, 1, 0): (0, 100, 0),
(0, 0, 1): (0, 0, 100),
(1, 1, 0): (30, 30, 0), # 900
(1, 0, 1): (30, 0, 30),
(0, 1, 1): (0, 30, 30),
(1, 1, 1): (15, 15, 15), # 3375
}
[docs]def clipped_range(data, portion=1.0, mode='central'):
"""
Determine range from data.
If *portion* is 1, use full range, otherwise use the center of the range
or the top of the range, depending on whether *mode* is 'central' or 'top'.
"""
if portion == 1.0:
return data.min(), data.max()
elif mode == 'central':
data = np.sort(data.flatten())
offset = int(portion*len(data)/2 + 0.5)
return data[offset], data[-offset]
elif mode == 'top':
data = np.sort(data.flatten())
offset = int(portion*len(data) + 0.5)
return data[offset], data[-1]
[docs]def draw_scattering(calculator, axes, view, jitter, dist='gaussian'):
"""
Plot the scattering for the particular view.
*calculator* is returned from :func:`build_model`. *axes* are the 3D axes
on which the data will be plotted. *view* and *jitter* are the current
orientation and orientation dispersity. *dist* is one of the sasmodels
weight distributions.
"""
if dist == 'uniform': # uniform is not yet in this branch
dist, scale = 'rectangle', 1/sqrt(3)
else:
scale = 1
# add the orientation parameters to the model parameters
theta, phi, psi = view
theta_pd, phi_pd, psi_pd = [scale*v for v in jitter]
theta_pd_n, phi_pd_n, psi_pd_n = PD_N_TABLE[(theta_pd > 0, phi_pd > 0, psi_pd > 0)]
## increase pd_n for testing jitter integration rather than simple viz
#theta_pd_n, phi_pd_n, psi_pd_n = [5*v for v in (theta_pd_n, phi_pd_n, psi_pd_n)]
pars = dict(
theta=theta, theta_pd=theta_pd, theta_pd_type=dist, theta_pd_n=theta_pd_n,
phi=phi, phi_pd=phi_pd, phi_pd_type=dist, phi_pd_n=phi_pd_n,
psi=psi, psi_pd=psi_pd, psi_pd_type=dist, psi_pd_n=psi_pd_n,
)
pars.update(calculator.pars)
# compute the pattern
qx, qy = calculator._data.x_bins, calculator._data.y_bins
Iqxy = calculator(**pars).reshape(len(qx), len(qy))
# scale it and draw it
Iqxy = np.log(Iqxy)
if calculator.limits:
# use limits from orientation (0,0,0)
vmin, vmax = calculator.limits
else:
vmax = Iqxy.max()
vmin = vmax*10**-7
#vmin, vmax = clipped_range(Iqxy, portion=portion, mode='top')
#print("range",(vmin,vmax))
#qx, qy = np.meshgrid(qx, qy)
if 0:
level = np.asarray(255*(Iqxy - vmin)/(vmax - vmin), 'i')
level[level < 0] = 0
colors = plt.get_cmap()(level)
axes.plot_surface(qx, qy, -1.1, rstride=1, cstride=1, facecolors=colors)
elif 1:
axes.contourf(qx/qx.max(), qy/qy.max(), Iqxy, zdir='z', offset=-1.1,
levels=np.linspace(vmin, vmax, 24))
else:
axes.pcolormesh(qx, qy, Iqxy)
[docs]def build_model(model_name, n=150, qmax=0.5, **pars):
"""
Build a calculator for the given shape.
*model_name* is any sasmodels model. *n* and *qmax* define an n x n mesh
on which to evaluate the model. The remaining parameters are stored in
the returned calculator as *calculator.pars*. They are used by
:func:`draw_scattering` to set the non-orientation parameters in the
calculation.
Returns a *calculator* function which takes a dictionary or parameters and
produces Iqxy. The Iqxy value needs to be reshaped to an n x n matrix
for plotting. See the :class:`sasmodels.direct_model.DirectModel` class
for details.
"""
from sasmodels.core import load_model_info, build_model as build_sasmodel
from sasmodels.data import empty_data2D
from sasmodels.direct_model import DirectModel
model_info = load_model_info(model_name)
model = build_sasmodel(model_info) #, dtype='double!')
q = np.linspace(-qmax, qmax, n)
data = empty_data2D(q, q)
calculator = DirectModel(data, model)
# stuff the values for non-orientation parameters into the calculator
calculator.pars = pars.copy()
calculator.pars.setdefault('backgound', 1e-3)
# fix the data limits so that we can see if the pattern fades
# under rotation or angular dispersion
Iqxy = calculator(theta=0, phi=0, psi=0, **calculator.pars)
Iqxy = np.log(Iqxy)
vmin, vmax = clipped_range(Iqxy, 0.95, mode='top')
calculator.limits = vmin, vmax+1
return calculator
[docs]def select_calculator(model_name, n=150, size=(10, 40, 100)):
"""
Create a model calculator for the given shape.
*model_name* is one of sphere, cylinder, ellipsoid, triaxial_ellipsoid,
parallelepiped or bcc_paracrystal. *n* is the number of points to use
in the q range. *qmax* is chosen based on model parameters for the
given model to show something intersting.
Returns *calculator* and tuple *size* (a,b,c) giving minor and major
equitorial axes and polar axis respectively. See :func:`build_model`
for details on the returned calculator.
"""
a, b, c = size
d_factor = 0.06 # for paracrystal models
if model_name == 'sphere':
calculator = build_model('sphere', n=n, radius=c)
a = b = c
elif model_name == 'sc_paracrystal':
a = b = c
dnn = c
radius = 0.5*c
calculator = build_model('sc_paracrystal', n=n, dnn=dnn,
d_factor=d_factor, radius=(1-d_factor)*radius,
background=0)
elif model_name == 'fcc_paracrystal':
a = b = c
# nearest neigbour distance dnn should be 2 radius, but I think the
# model uses lattice spacing rather than dnn in its calculations
dnn = 0.5*c
radius = sqrt(2)/4 * c
calculator = build_model('fcc_paracrystal', n=n, dnn=dnn,
d_factor=d_factor, radius=(1-d_factor)*radius,
background=0)
elif model_name == 'bcc_paracrystal':
a = b = c
# nearest neigbour distance dnn should be 2 radius, but I think the
# model uses lattice spacing rather than dnn in its calculations
dnn = 0.5*c
radius = sqrt(3)/2 * c
calculator = build_model('bcc_paracrystal', n=n, dnn=dnn,
d_factor=d_factor, radius=(1-d_factor)*radius,
background=0)
elif model_name == 'cylinder':
calculator = build_model('cylinder', n=n, qmax=0.3, radius=b, length=c)
a = b
elif model_name == 'ellipsoid':
calculator = build_model('ellipsoid', n=n, qmax=1.0,
radius_polar=c, radius_equatorial=b)
a = b
elif model_name == 'triaxial_ellipsoid':
calculator = build_model('triaxial_ellipsoid', n=n, qmax=0.5,
radius_equat_minor=a,
radius_equat_major=b,
radius_polar=c)
elif model_name == 'parallelepiped':
calculator = build_model('parallelepiped', n=n, a=a, b=b, c=c)
else:
raise ValueError("unknown model %s"%model_name)
return calculator, (a, b, c)
SHAPES = [
'parallelepiped',
'sphere', 'ellipsoid', 'triaxial_ellipsoid',
'cylinder',
'fcc_paracrystal', 'bcc_paracrystal', 'sc_paracrystal',
]
DRAW_SHAPES = {
'fcc_paracrystal': draw_fcc,
'bcc_paracrystal': draw_bcc,
'sc_paracrystal': draw_sc,
'parallelepiped': draw_parallelepiped,
}
DISTRIBUTIONS = [
'gaussian', 'rectangle', 'uniform',
]
DIST_LIMITS = {
'gaussian': 30,
'rectangle': 90/sqrt(3),
'uniform': 90,
}
[docs]def run(model_name='parallelepiped', size=(10, 40, 100),
dist='gaussian', mesh=30,
projection='equirectangular'):
"""
Show an interactive orientation and jitter demo.
*model_name* is one of: sphere, ellipsoid, triaxial_ellipsoid,
parallelepiped, cylinder, or sc/fcc/bcc_paracrystal
*size* gives the dimensions (a, b, c) of the shape.
*dist* is the type of dispersition: gaussian, rectangle, or uniform.
*mesh* is the number of points in the dispersion mesh.
*projection* is the map projection to use for the mesh: equirectangular,
sinusoidal, guyou, azimuthal_equidistance, or azimuthal_equal_area.
"""
# projection number according to 1-order position in list, but
# only 1 and 2 are implemented so far.
from sasmodels import generate
generate.PROJECTION = PROJECTIONS.index(projection) + 1
if generate.PROJECTION > 2:
print("*** PROJECTION %s not implemented in scattering function ***"%projection)
generate.PROJECTION = 2
# set up calculator
calculator, size = select_calculator(model_name, n=150, size=size)
draw_shape = DRAW_SHAPES.get(model_name, draw_parallelepiped)
## uncomment to set an independent the colour range for every view
## If left commented, the colour range is fixed for all views
calculator.limits = None
## initial view
#theta, dtheta = 70., 10.
#phi, dphi = -45., 3.
#psi, dpsi = -45., 3.
theta, phi, psi = 0, 0, 0
dtheta, dphi, dpsi = 0, 0, 0
## create the plot window
#plt.hold(True)
plt.subplots(num=None, figsize=(5.5, 5.5))
plt.set_cmap('gist_earth')
plt.clf()
plt.gcf().canvas.set_window_title(projection)
#gs = gridspec.GridSpec(2,1,height_ratios=[4,1])
#axes = plt.subplot(gs[0], projection='3d')
axes = plt.axes([0.0, 0.2, 1.0, 0.8], projection='3d')
try: # CRUFT: not all versions of matplotlib accept 'square' 3d projection
axes.axis('square')
except Exception:
pass
axcolor = 'lightgoldenrodyellow'
## add control widgets to plot
axes_theta = plt.axes([0.1, 0.15, 0.45, 0.04], axisbg=axcolor)
axes_phi = plt.axes([0.1, 0.1, 0.45, 0.04], axisbg=axcolor)
axes_psi = plt.axes([0.1, 0.05, 0.45, 0.04], axisbg=axcolor)
stheta = Slider(axes_theta, 'Theta', -90, 90, valinit=theta)
sphi = Slider(axes_phi, 'Phi', -180, 180, valinit=phi)
spsi = Slider(axes_psi, 'Psi', -180, 180, valinit=psi)
axes_dtheta = plt.axes([0.75, 0.15, 0.15, 0.04], axisbg=axcolor)
axes_dphi = plt.axes([0.75, 0.1, 0.15, 0.04], axisbg=axcolor)
axes_dpsi = plt.axes([0.75, 0.05, 0.15, 0.04], axisbg=axcolor)
# Note: using ridiculous definition of rectangle distribution, whose width
# in sasmodels is sqrt(3) times the given width. Divide by sqrt(3) to keep
# the maximum width to 90.
dlimit = DIST_LIMITS[dist]
sdtheta = Slider(axes_dtheta, 'dTheta', 0, 2*dlimit, valinit=dtheta)
sdphi = Slider(axes_dphi, 'dPhi', 0, 2*dlimit, valinit=dphi)
sdpsi = Slider(axes_dpsi, 'dPsi', 0, 2*dlimit, valinit=dpsi)
## callback to draw the new view
def update(val, axis=None):
view = stheta.val, sphi.val, spsi.val
jitter = sdtheta.val, sdphi.val, sdpsi.val
# set small jitter as 0 if multiple pd dims
dims = sum(v > 0 for v in jitter)
limit = [0, 0.5, 5][dims]
jitter = [0 if v < limit else v for v in jitter]
axes.cla()
draw_beam(axes, (0, 0))
draw_jitter(axes, view, jitter, dist=dist, size=size, draw_shape=draw_shape)
#draw_jitter(axes, view, (0,0,0))
draw_mesh(axes, view, jitter, dist=dist, n=mesh, projection=projection)
draw_scattering(calculator, axes, view, jitter, dist=dist)
plt.gcf().canvas.draw()
## bind control widgets to view updater
stheta.on_changed(lambda v: update(v, 'theta'))
sphi.on_changed(lambda v: update(v, 'phi'))
spsi.on_changed(lambda v: update(v, 'psi'))
sdtheta.on_changed(lambda v: update(v, 'dtheta'))
sdphi.on_changed(lambda v: update(v, 'dphi'))
sdpsi.on_changed(lambda v: update(v, 'dpsi'))
## initialize view
update(None, 'phi')
## go interactive
plt.show()
[docs]def main():
parser = argparse.ArgumentParser(
description="Display jitter",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument('-p', '--projection', choices=PROJECTIONS,
default=PROJECTIONS[0],
help='coordinate projection')
parser.add_argument('-s', '--size', type=str, default='10,40,100',
help='a,b,c lengths')
parser.add_argument('-d', '--distribution', choices=DISTRIBUTIONS,
default=DISTRIBUTIONS[0],
help='jitter distribution')
parser.add_argument('-m', '--mesh', type=int, default=30,
help='#points in theta-phi mesh')
parser.add_argument('shape', choices=SHAPES, nargs='?', default=SHAPES[0],
help='oriented shape')
opts = parser.parse_args()
size = tuple(int(v) for v in opts.size.split(','))
run(opts.shape, size=size,
mesh=opts.mesh, dist=opts.distribution,
projection=opts.projection)
if __name__ == "__main__":
main()