Index: .gitignore
===================================================================
--- .gitignore	(revision 8a5f0216970a47f3e897a508054840132bd87909)
+++ .gitignore	(revision e9ed2de3b1f1923285ba20e8d7898416bbf6c827)
@@ -22,2 +22,5 @@
 /example/Fit_*/
 /example/batch_fit.csv
+# Note: changes to gauss20, gauss76 and gauss150 are still tracked since they
+# are part of the repo and required by some models.
+/sasmodels/models/lib/gauss*.c
Index: .travis.yml
===================================================================
--- .travis.yml	(revision ce8c38870db377607bac6fe577399e8ec56f634c)
+++ .travis.yml	(revision 2d09df1a016d5aba6f689a4b863e9e4314756ac0)
@@ -6,4 +6,5 @@
     env:
     - PY=2.7
+    - DEPLOY=True
   - os: linux
     env:
@@ -51,4 +52,5 @@
   on:
     branch: master
+    condition: $DEPLOY = True
 notifications:
   slack:
Index: appveyor.yml
===================================================================
--- appveyor.yml	(revision d810d9681c488004f96e5215d71b8693faed856f)
+++ appveyor.yml	(revision fefc185ba4c27aefa57a3fb46c7866f0a7a0d4c9)
@@ -4,5 +4,6 @@
   # /E:ON and /V:ON options are not enabled in the batch script interpreter
   # See: http://stackoverflow.com/a/13751649/163740
-  CMD_IN_ENV: "cmd /E:ON /V:ON /C obvci_appveyor_python_build_env.cmd"
+  # 2018-01-19 PAK: probably irrelevant now that we are using tinycc rather than msvc
+  #CMD_IN_ENV: "cmd /E:ON /V:ON /C obvci_appveyor_python_build_env.cmd"
 
   # Workaround for https://github.com/conda/conda-build/issues/636
@@ -22,5 +23,5 @@
     - TARGET_ARCH: "x86"
       CONDA_PY: "35"
-      PY_CONDITION: "python >=3.5"
+      PY_CONDITION: "python >=3.5,<3.6"
       CONDA_INSTALL_LOCN: "C:\\Miniconda35"
     - TARGET_ARCH: "x64"
@@ -34,5 +35,5 @@
     - TARGET_ARCH: "x64"
       CONDA_PY: "35"
-      PY_CONDITION: "python >=3.5"
+      PY_CONDITION: "python >=3.5,<3.6"
       CONDA_INSTALL_LOCN: "C:\\Miniconda35-x64"
 
@@ -43,9 +44,14 @@
 
 install:
-    # Set the CONDA_NPY, although it has no impact on the actual build. We need this because of a test within conda-build.
+    # Set the CONDA_NPY, although it has no impact on the actual build. 
+    # We need this because of a test within conda-build.
     - cmd: set CONDA_NPY=19
 
     # Remove cygwin (and therefore the git that comes with it).
-    - cmd: rmdir C:\cygwin /s /q
+    # 2018-01-19 PAK: irrelevant since we already pulled the repo
+    #- cmd: rmdir C:\cygwin /s /q
+
+    # Set the conda path; would be nice to do this
+    - cmd: path %CONDA_INSTALL_LOCN%\Scripts;%PATH%
 
     # Use the pre-installed Miniconda for the desired arch
@@ -56,23 +62,27 @@
     # so that we can update it. Then we remove it so that
     # we can do a proper activation.
-    - cmd: set "OLDPATH=%PATH%"
-    - cmd: set "PATH=%CONDA_INSTALL_LOCN%\\Scripts;%CONDA_INSTALL_LOCN%\\Library\\bin;%PATH%"
     - cmd: conda update --yes --quiet conda python
-    - cmd: set "PATH=%OLDPATH%"
     - cmd: call %CONDA_INSTALL_LOCN%\Scripts\activate.bat
-
-    - cmd: conda config --add channels conda-forge
-    - cmd: conda config --set show_channel_urls true
-    - cmd: conda update --yes --quiet conda
-    - cmd: conda install --yes --quiet obvious-ci
-    - cmd: conda install --yes --quiet numpy toolchain scipy cython cffi
-    - cmd: conda install --yes --channel conda-forge pyopencl
+    #- cmd: conda config --add channels conda-forge
+    #- cmd: conda config --set show_channel_urls true
+    #- cmd: conda install --yes --quiet obvious-ci
+    # 2018-01-19 PAK: skipping toolchain cython and cffi (these were for pyopencl?)
+    - cmd: conda install --yes --quiet numpy scipy
+    # 2018-01-19 PAK: skipping pyopencl; this would be needed for deploy but maybe not for test
+    #- cmd: conda install --yes --channel conda-forge pyopencl
+    # 2018-01-19 PAK: 3rd party packages might need msvc, so %CMD_IN_ENV% may be needed for pip
     - cmd: pip install bumps unittest-xml-reporting tinycc
 
 build_script:
     # Build the project
-    - "%CMD_IN_ENV% python setup.py build"
+    # 2018-01-19 PAK: maybe need one of this if using msvc?
+    #- "%CMD_IN_ENV% python setup.py build"
+    #- cmd /E:ON /V:ON /C obvci_appveyor_python_build_env.cmd python setup.py build
+    - python setup.py build
 
 test_script:
     # Run the project tests
-    - "%CMD_IN_ENV% python -m sasmodels.model_test dll all"
+    # 2018-01-19 PAK: maybe need one of this if using msvc?
+    #- "%CMD_IN_ENV% python -m sasmodels.model_test dll all"
+    #- cmd /E:ON /V:ON /C obvci_appveyor_python_build_env.cmd python -m sasmoels.model_test dll all
+    - python -m sasmodels.model_test dll all
Index: deploy.sh
===================================================================
--- deploy.sh	(revision 2ba5ba52c4d92ea0df8d6a2575e06a4e0dcd6885)
+++ deploy.sh	(revision 88096f552748890cc8b1ab0836eb234c560caf06)
Index: doc/developer/overview.rst
===================================================================
--- doc/developer/overview.rst	(revision 3d4083969c45f693ad16ca85a10880859ed708e7)
+++ doc/developer/overview.rst	(revision 0d5a655d6ddebe6096fad050347df4796b956767)
@@ -185,4 +185,12 @@
 jitter applied before particle orientation.
 
+When computing the orientation dispersity integral, the weights for
+the individual points depends on the map projection used to translate jitter
+angles into latitude/longitude.  The choice of projection is set by
+*sasmodels.generate.PROJECTION*, with the default *PROJECTION=1* for
+equirectangular and *PROJECTION=2* for sinusoidal.  The more complicated
+Guyou and Postel projections are not implemented. See jitter.draw_mesh
+for details.
+
 For numerical integration within form factors etc. sasmodels is mostly using
 Gaussian quadrature with 20, 76 or 150 points depending on the model. It also
@@ -199,12 +207,41 @@
 Useful testing routines include:
 
-:mod:`asymint` a direct implementation of the surface integral for certain
-models to get a more trusted value for the 1D integral using a
-reimplementation of the 2D kernel in python and mpmath (which computes math
-functions to arbitrary precision). It uses $\theta$ ranging from 0 to $\pi$
-and $\phi$ ranging from 0 to $2\pi$. It perhaps would benefit from including
-the U-substitution for $\theta$.
-
-:mod:`check1d` uses sasmodels 1D integration and compares that with a
+The *sascomp* utility is used to view and compare models with different
+parameters and calculation engines. The usual case is to simply plot a
+model that you are developing::
+
+    python sascomp path/to/model.py
+
+Once the obvious problems are addressed, check the numerical precision
+across a variety of randomly generated inputs::
+
+    python sascomp -engine=single,double path/to/model.py -sets=10
+
+You can compare different parameter values for the same or different models.
+For example when looking along the long axis of a cylinder ($\theta=0$),
+dispersity in $\theta$ should be equivalent to dispersity in $\phi$
+when $\phi=90$::
+
+    python sascomp -2d cylinder theta=0 phi=0,90 theta_pd_type=rectangle \\
+    phi_pd_type=rectangle phi_pd=10,1 theta_pd=1,10 length=500 radius=10
+
+It turns out that they are not because the equirectangular map projection
+weights the points by $\cos(\theta)$ so $\Delta\theta$ is not identical
+to $\Delta\phi$.  Setting *PROJECTION=2* in :mod:`sasmodels.generate` helps
+somewhat.  Postel would help even more in this case, though leading
+to distortions elsewhere.  See :mod:`sasmodels.compare` for many more details.
+
+*sascomp -ngauss=n* allows you to set the number of quadrature points used
+for the 1D integration for any model.  For example, a carbon nanotube with
+length 10 $\mu$\ m and radius 1 nm is not computed correctly at high $q$::
+
+    python sascomp cylinder length=100000 radius=10 -ngauss=76,500 -double -highq
+
+Note: ticket 702 gives some forms for long rods and thin disks that may
+be used in place of the integration, depending on $q$, radius and length; if
+the cylinder model is updated with these corrections then above call will show
+no difference.
+
+*explore/check1d.py* uses sasmodels 1D integration and compares that with a
 rectangle distribution in $\theta$ and $\phi$, with $\theta$ limits set to
 $\pm 90/\sqrt(3)$ and $\phi$ limits set to $\pm 180/\sqrt(3)$ [The rectangle
@@ -214,33 +251,57 @@
 similar reasoning.] This should rotate the sample through the entire
 $\theta$-$\phi$ surface according to the pattern that you see in jitter.py when
-you modify it to use 'rectangle' rather than 'gaussian' for its distribution
-without changing the viewing angle. In order to match the 1-D pattern for
-an arbitrary viewing angle on triaxial shapes, we need to integrate
+you use 'rectangle' rather than 'gaussian' for its distribution without
+changing the viewing angle. In order to match the 1-D pattern for an arbitrary
+viewing angle on triaxial shapes, we need to integrate
 over $\theta$, $\phi$ and $\Psi$.
 
-When computing the dispersity integral, weights are scaled by
-$|\cos(\delta \theta)|$ to account for the points in $\phi$ getting closer
-together as $\delta \theta$ increases.
-[This will probably change so that instead of adjusting the weights, we will
-adjust $\delta\theta$-$\delta\phi$ mesh so that the point density in
-$\delta\phi$ is lower at larger $\delta\theta$. The flag USE_SCALED_PHI in
-*kernel_iq.c* selects an alternative algorithm.]
-
-The integrated dispersion is computed at a set of $(qx, qy)$ points $(q
-\cos(\alpha), q \sin(\alpha))$ at some angle $\alpha$ (currently angle=0) for
-each $q$ used in the 1-D integration. The individual $q$ points should be
-equivalent to asymint rect-n when the viewing angle is set to
-$(\theta,\phi,\Psi) = (90, 0, 0)$. Such tests can help to validate that 2d
-models are consistent with 1d models.
-
-:mod:`sascomp -sphere=n` uses the same rectangular distribution as check1d to
-compute the pattern of the $q_x$-$q_y$ grid.
-
-The :mod:`sascomp` utility can be used for 2d as well as 1d calculations to
-compare results for two sets of parameters or processor types, for example
-these two oriented cylinders here should be equivalent.
-
-:mod:`\./sascomp -2d cylinder theta=0 phi=0,90 theta_pd_type=rectangle phi_pd_type=rectangle phi_pd=10,1 theta_pd=1,10 length=500 radius=10`
-
+*sascomp -sphere=n* uses the same rectangular distribution as check1d to
+compute the pattern of the $q_x$-$q_y$ grid.  This ought to produce a
+spherically symmetric pattern.
+
+*explore/precision.py* investigates the accuracy of individual functions
+on the different execution platforms.  This includes the basic special
+functions as well as more complex expressions used in scattering.  In many
+cases the OpenCL function in sasmodels will use a polynomial approximation
+over part of the range to improve accuracy, as shown in::
+
+    python explore/precision.py 3j1/x
+
+*explore/realspace.py* allows you to set up a Monte Carlo simulation of your
+model by sampling random points within and computing the 1D and 2D scattering
+patterns.  This was used to check the core shell parallelepiped example.  This
+is similar to the general sas calculator in sasview, though it uses different
+code.
+
+*sasmodels/jitter.py* is for exploring different options for handling
+orientation and orientation dispersity.  It uses *sasmodels/guyou.py* to
+generate the Guyou projection.
+
+*explore/asymint.py* is a direct implementation of the 1D integration for
+a number of different models.  It calculates the integral for a particular
+$q$ using several different integration schemes, including mpmath with 100
+bits of precision (33 digits), so you can use it to check the target values
+for the 1D model tests.  This is not a general purpose tool; you will need to
+edit the file to change the model parameters. It does not currently
+apply the $u=cos(\theta)$ substitution that many models are using
+internally so the 76-point gaussian quadrature may not match the value
+produced by the eqivalent model from sasmodels.
+
+*explore/symint.py* is for rotationally symmetric models (just cylinder for
+now), so it can compute an entire curve rather than a single $q$ point.  It
+includes code to compare the long cylinder approximation to cylinder.
+
+*explore/rpa.py* is for checking different implementations of the RPA model
+against calculations for specific blends.  This is a work in (suspended)
+progress.
+
+There are a few modules left over in *explore* that can probably be removed.
+*angular_pd.py* was an early version of *jitter.py*.  *sc.py* and *sc.c*
+was used to explore different calculations for paracrystal models; it has
+been absorbed into *asymint.py*. *transform_angles.py* translates orientation
+parameters from the SasView 3.x definition to sasmodels.
+
+*explore/angles.py* generates code for the view and jitter transformations.
+Keep this around since it may be needed if we add new projections.
 
 Testing
Index: doc/genapi.py
===================================================================
--- doc/genapi.py	(revision 706f46620c115bdbc319d902ba7678c9d247d6d0)
+++ doc/genapi.py	(revision 0d5a655d6ddebe6096fad050347df4796b956767)
@@ -69,4 +69,6 @@
     ('exception', 'Annotate exceptions'),
     ('generate', 'Model parser'),
+    ('jitter', 'Orientation explorer'),
+    ('guyou', 'Guyou map projection'),
     ('kernel', 'Evaluator type definitions'),
     ('kernelcl', 'OpenCL model evaluator'),
Index: doc/guide/orientation/orientation.rst
===================================================================
--- doc/guide/orientation/orientation.rst	(revision 82592da3721b7e2098cf6346cf224814189fadb3)
+++ doc/guide/orientation/orientation.rst	(revision 0d5a655d6ddebe6096fad050347df4796b956767)
@@ -4,16 +4,21 @@
 ==================
 
-With two dimensional small angle diffraction data SasView will calculate
+With two dimensional small angle diffraction data sasmodels will calculate
 scattering from oriented particles, applicable for example to shear flow
 or orientation in a magnetic field.
 
 In general we first need to define the reference orientation
-of the particles with respect to the incoming neutron or X-ray beam. This
-is done using three angles: $\theta$ and $\phi$ define the orientation of
-the axis of the particle, angle $\Psi$ is defined as the orientation of
-the major axis of the particle cross section with respect to its starting
-position along the beam direction. The figures below are for an elliptical
-cross section cylinder, but may be applied analogously to other shapes of
-particle.
+of the particle's $a$-$b$-$c$ axes with respect to the incoming
+neutron or X-ray beam. This is done using three angles: $\theta$ and $\phi$
+define the orientation of the $c$-axis of the particle, and angle $\Psi$ is
+defined as the orientation of the major axis of the particle cross section
+with respect to its starting position along the beam direction (or
+equivalently, as rotation about the $c$ axis). There is an unavoidable
+ambiguity when $c$ is aligned with $z$ in that $\phi$ and $\Psi$ both
+serve to rotate the particle about $c$, but this symmetry is destroyed
+when $\theta$ is not a multiple of 180.
+
+The figures below are for an elliptical cross section cylinder, but may
+be applied analogously to other shapes of particle.
 
 .. note::
@@ -29,8 +34,8 @@
 
     Definition of angles for oriented elliptical cylinder, where axis_ratio
-    b/a is shown >1, Note that rotation $\theta$, initially in the $x$-$z$
+    b/a is shown >1. Note that rotation $\theta$, initially in the $x$-$z$
     plane, is carried out first, then rotation $\phi$ about the $z$-axis,
     finally rotation $\Psi$ is around the axis of the cylinder. The neutron
-    or X-ray beam is along the $z$ axis.
+    or X-ray beam is along the $-z$ axis.
 
 .. figure::
@@ -40,57 +45,161 @@
     with $\Psi$ = 0.
 
-Having established the mean direction of the particle we can then apply
-angular orientation distributions. This is done by a numerical integration
-over a range of angles in a similar way to particle size dispersity.
-In the current version of sasview the orientational dispersity is defined
-with respect to the axes of the particle.
+Having established the mean direction of the particle (the view) we can then
+apply angular orientation distributions (jitter). This is done by a numerical
+integration over a range of angles in a similar way to particle size
+dispersity. The orientation dispersity is defined with respect to the
+$a$-$b$-$c$ axes of the particle, with roll angle $\Psi$ about the $c$-axis,
+yaw angle $\theta$ about the $b$-axis and pitch angle $\phi$ about the
+$a$-axis.
+
+You can explore the view and jitter angles interactively using
+:func:`sasmodels.jitter.run`.  Enter the following into the python
+interpreter::
+
+    from sasmodels import jitter
+    jitter.run()
+
+More formally, starting with axes $a$-$b$-$c$ of the particle aligned
+with axes $x$-$y$-$z$ of the laboratory frame, the orientation dispersity
+is applied first, using the
+`Tait-Bryan <https://en.wikipedia.org/wiki/Euler_angles#Conventions_2>`_
+$x$-$y'$-$z''$ convention with angles $\Delta\phi$-$\Delta\theta$-$\Delta\Psi$.
+The reference orientation then follows, using the
+`Euler angles <https://en.wikipedia.org/wiki/Euler_angles#Conventions>`_
+$z$-$y'$-$z''$ with angles $\phi$-$\theta$-$\Psi$.  This is implemented
+using rotation matrices as
+
+.. math::
+
+    R = R_z(\phi)\, R_y(\theta)\, R_z(\Psi)\,
+        R_x(\Delta\phi)\, R_y(\Delta\theta)\, R_z(\Delta\Psi)
+
+To transform detector $(q_x, q_y)$ values into $(q_a, q_b, q_c)$ for the
+shape in its canonical orientation, use
+
+.. math::
+
+    [q_a, q_b, q_c]^T = R^{-1} \, [q_x, q_y, 0]^T
+
+
+The inverse rotation is easily calculated by rotating the opposite directions
+in the reverse order, so
+
+.. math::
+
+    R^{-1} = R_z(-\Delta\Psi)\, R_y(-\Delta\theta)\, R_x(-\Delta\phi)\,
+             R_z(-\Psi)\, R_y(-\theta)\, R_z(-\phi)
+
 
 The $\theta$ and $\phi$ orientation parameters for the cylinder only appear
-when fitting 2d data. On introducing "Orientational Distribution" in
-the angles, "distribution of theta" and "distribution of phi" parameters will
+when fitting 2d data. On introducing "Orientational Distribution" in the
+angles, "distribution of theta" and "distribution of phi" parameters will
 appear. These are actually rotations about the axes $\delta_1$ and $\delta_2$
-of the cylinder, the $b$ and $a$ axes of the cylinder cross section. (When
-$\theta = \phi = 0$ these are parallel to the $Y$ and $X$ axes of the
-instrument.) The third orientation distribution, in $\Psi$, is about the $c$
-axis of the particle. Some experimentation may be required to understand the
-2d patterns fully. A number of different shapes of distribution are
-available, as described for polydispersity, see :ref:`polydispersityhelp` .
-
-Earlier versions of SasView had numerical integration issues in some
-circumstances when distributions passed through 90 degrees. The distributions
-in particle coordinates are more robust, but should still be approached with
-care for large ranges of angle.
+of the cylinder, which correspond to the $b$ and $a$ axes of the cylinder
+cross section. (When $\theta = \phi = 0$ these are parallel to the $Y$ and
+$X$ axes of the instrument.) The third orientation distribution, in $\Psi$,
+is about the $c$ axis of the particle. Some experimentation may be required
+to understand the 2d patterns fully. A number of different shapes of
+distribution are available, as described for size dispersity, see
+:ref:`polydispersityhelp`.
+
+Given that the angular dispersion distribution is defined in cartesian space,
+over a cube defined by
+
+.. math::
+
+    [-\Delta \theta, \Delta \theta] \times
+    [-\Delta \phi, \Delta \phi] \times
+    [-\Delta \Psi, \Delta \Psi]
+
+but the orientation is defined over a sphere, we are left with a
+`map projection <https://en.wikipedia.org/wiki/List_of_map_projections>`_
+problem, with different tradeoffs depending on how values in $\Delta\theta$
+and $\Delta\phi$ are translated into latitude/longitude on the sphere.
+
+Sasmodels is using the
+`equirectangular projection <https://en.wikipedia.org/wiki/Equirectangular_projection>`_.
+In this projection, square patches in angular dispersity become wedge-shaped
+patches on the sphere. To correct for the changing point density, there is a
+scale factor of $\sin(\Delta\theta)$ that applies to each point in the
+integral. This is not enough, though. Consider a shape which is tumbling
+freely around the $b$ axis, with $\Delta\theta$ uniform in $[-180, 180]$. At
+$\pm 90$, all points in $\Delta\phi$ map to the pole, so the jitter will have
+a distinct angular preference. If the spin axis is along the beam (which
+will be the case for $\theta=90$ and $\Psi=90$) the scattering pattern
+should be circularly symmetric, but it will go to zero at $q_x = 0$ due to the
+$\sin(\Delta\theta)$ correction. This problem does not appear for a shape
+that is tumbling freely around the $a$ axis, with $\Delta\phi$ uniform in
+$[-180, 180]$, so swap the $a$ and $b$ axes so $\Delta\theta < \Delta\phi$
+and adjust $\Psi$ by 90. This works with the current sasmodels shapes due to
+symmetry.
+
+Alternative projections were considered.
+The `sinusoidal projection <https://en.wikipedia.org/wiki/Sinusoidal_projection>`_
+works by scaling $\Delta\phi$ as $\Delta\theta$ increases, and dropping those
+points outside $[-180, 180]$. The distortions are a little less for middle
+ranges of $\Delta\theta$, but they are still severe for large $\Delta\theta$
+and the model is much harder to explain.
+The `azimuthal equidistance projection <https://en.wikipedia.org/wiki/Azimuthal_equidistant_projection>`_
+also improves on the equirectangular projection by extending the range of
+reasonable values for the $\Delta\theta$ range, with $\Delta\phi$ forming a
+wedge that cuts to the opposite side of the sphere rather than cutting to the
+pole. This projection has the nice property that distance from the center are
+preserved, and that $\Delta\theta$ and $\Delta\phi$ act the same.
+The `azimuthal equal area projection <https://en.wikipedia.org/wiki/Lambert_azimuthal_equal-area_projection>`_
+is like the azimuthal equidistance projection, but it preserves area instead
+of distance. It also has the same behaviour for $\Delta\theta$ and $\Delta\phi$.
+The `Guyou projection <https://en.wikipedia.org/wiki/Guyou_hemisphere-in-a-square_projection>`_
+has an excellent balance with reasonable distortion in both $\Delta\theta$
+and $\Delta\phi$, as well as preserving small patches. However, it requires
+considerably more computational overhead, and we have not yet derived the
+formula for the distortion correction, measuring the degree of stretch at
+the point $(\Delta\theta, \Delta\phi)$ on the map.
 
 .. note::
-    Note that the form factors for oriented particles are also performing
-    numerical integrations over one or more variables, so care should be taken,
-    especially with very large particles or more extreme aspect ratios. In such 
-    cases results may not be accurate, particularly at very high Q, unless the model
-    has been specifically coded to use limiting forms of the scattering equations.
-    
-    For best numerical results keep the $\theta$ distribution narrower than the $\phi$ 
-    distribution. Thus for asymmetric particles, such as elliptical_cylinder, you may 
-    need to reorder the sizes of the three axes to acheive the desired result. 
-    This is due to the issues of mapping a rectangular distribution onto the 
-    surface of a sphere.
-
-Users can experiment with the values of *Npts* and *Nsigs*, the number of steps 
-used in the integration and the range spanned in number of standard deviations. 
-The standard deviation is entered in units of degrees. For a "rectangular" 
-distribution the full width should be $\pm \sqrt(3)$ ~ 1.73 standard deviations. 
-The new "uniform" distribution avoids this by letting you directly specify the 
+    Note that the form factors for oriented particles are performing
+    numerical integrations over one or more variables, so care should be
+    taken, especially with very large particles or more extreme aspect
+    ratios. In such cases results may not be accurate, particularly at very
+    high Q, unless the model has been specifically coded to use limiting
+    forms of the scattering equations.
+
+    For best numerical results keep the $\theta$ distribution narrower than
+    the $\phi$ distribution. Thus for asymmetric particles, such as
+    elliptical_cylinder, you may need to reorder the sizes of the three axes
+    to acheive the desired result. This is due to the issues of mapping a
+    rectanglar distribution onto the surface of a sphere.
+
+Users can experiment with the values of *Npts* and *Nsigs*, the number of steps
+used in the integration and the range spanned in number of standard deviations.
+The standard deviation is entered in units of degrees. For a "rectangular"
+distribution the full width should be $\pm \sqrt(3)$ ~ 1.73 standard deviations.
+The new "uniform" distribution avoids this by letting you directly specify the
 half width.
 
-The angular distributions will be truncated outside of the range -180 to +180 
-degrees, so beware of using saying a broad Gaussian distribution with large value
-of *Nsigs*, as the array of *Npts* may be truncated to many fewer points than would 
-give a good integration,as well as becoming rather meaningless. (At some point 
-in the future the actual polydispersity arrays may be made available to the user 
-for inspection.)
+The angular distributions may be truncated outside of the range -180 to +180
+degrees, so beware of using saying a broad Gaussian distribution with large
+value of *Nsigs*, as the array of *Npts* may be truncated to many fewer
+points than would give a good integration,as well as becoming rather
+meaningless. (At some point in the future the actual dispersion arrays may be
+made available to the user for inspection.)
 
 Some more detailed technical notes are provided in the developer section of
 this manual :ref:`orientation_developer` .
 
+This definition of orientation is new to SasView 4.2.  In earlier versions,
+the orientation distribution appeared as a distribution of view angles.
+This led to strange effects when $c$ was aligned with $z$, where changes
+to the $\phi$ angle served only to rotate the shape about $c$, rather than
+having a consistent interpretation as the pitch of the shape relative to
+the flow field defining the reference orientation.  Prior to SasView 4.1,
+the reference orientation was defined using a Tait-Bryan convention, making
+it difficult to control.  Now, rotation in $\theta$ modifies the spacings
+in the refraction pattern, and rotation in $\phi$ rotates it in the detector
+plane.
+
+
 *Document History*
 
 | 2017-11-06 Richard Heenan
+| 2017-12-20 Paul Kienzle
Index: doc/guide/plugin.rst
===================================================================
--- doc/guide/plugin.rst	(revision 3048ec65c991d5665c396efe7de93e067fce4707)
+++ doc/guide/plugin.rst	(revision 7e6bc45eddf06819ea47d06f521d0c27c582996e)
@@ -292,6 +292,7 @@
 **Note: The order of the parameters in the definition will be the order of the
 parameters in the user interface and the order of the parameters in Iq(),
-Iqxy() and form_volume(). And** *scale* **and** *background* **parameters are
-implicit to all models, so they do not need to be included in the parameter table.**
+Iqac(), Iqabc() and form_volume(). And** *scale* **and** *background*
+**parameters are implicit to all models, so they do not need to be included
+in the parameter table.**
 
 - **"name"** is the name of the parameter shown on the FitPage.
@@ -362,9 +363,11 @@
     scattered intensity.
 
-  - "volume" parameters are passed to Iq(), Iqxy(), and form_volume(), and
-    have polydispersity loops generated automatically.
-
-  - "orientation" parameters are only passed to Iqxy(), and have angular
-    dispersion.
+  - "volume" parameters are passed to Iq(), Iqac(), Iqabc() and form_volume(),
+    and have polydispersity loops generated automatically.
+
+  - "orientation" parameters are not passed, but instead are combined with
+    orientation dispersity to translate *qx* and *qy* to *qa*, *qb* and *qc*.
+    These parameters should appear at the end of the table with the specific
+    names *theta*, *phi* and for asymmetric shapes *psi*, in that order.
 
 Some models will have integer parameters, such as number of pearls in the
@@ -419,7 +422,5 @@
 That is, the individual models do not need to include polydispersity
 calculations, but instead rely on numerical integration to compute the
-appropriately smeared pattern.   Angular dispersion values over polar angle
-$\theta$ requires an additional $\cos \theta$ weighting due to decreased
-arc length for the equatorial angle $\phi$ with increasing latitude.
+appropriately smeared pattern.
 
 Python Models
@@ -468,8 +469,4 @@
 barbell).  If I(q; pars) is NaN for any $q$, then those parameters will be
 ignored, and not included in the calculation of the weighted polydispersity.
-
-Similar to *Iq*, you can define *Iqxy(qx, qy, par1, par2, ...)* where the
-parameter list includes any orientation parameters.  If *Iqxy* is not defined,
-then it will default to *Iqxy = Iq(sqrt(qx**2+qy**2), par1, par2, ...)*.
 
 Models should define *form_volume(par1, par2, ...)* where the parameter
@@ -497,16 +494,16 @@
     }
 
-*Iqxy* is similar to *Iq*, except it uses parameters *qx, qy* instead of *q*,
-and it includes orientation parameters.
-
 *form_volume* defines the volume of the shape. As in python models, it
 includes only the volume parameters.
 
-*Iqxy* will default to *Iq(sqrt(qx**2 + qy**2), par1, ...)* and
-*form_volume* will default to 1.0.
-
 **source=['fn.c', ...]** includes the listed C source files in the
-program before *Iq* and *Iqxy* are defined. This allows you to extend the
-library of C functions available to your model.
+program before *Iq* and *form_volume* are defined. This allows you to
+extend the library of C functions available to your model.
+
+*c_code* includes arbitrary C code into your kernel, which can be
+handy for defining helper functions for *Iq* and *form_volume*. Note that
+you can put the full function definition for *Iq* and *form_volume*
+(include function declaration) into *c_code* as well, or put them into an
+external C file and add that file to the list of sources.
 
 Models are defined using double precision declarations for the
@@ -533,4 +530,123 @@
     #define INVALID(v) (v.bell_radius < v.radius)
 
+The INVALID define can go into *Iq*, or *c_code*, or an external C file
+listed in *source*.
+
+Oriented Shapes
+...............
+
+If the scattering is dependent on the orientation of the shape, then you
+will need to include *orientation* parameters *theta*, *phi* and *psi*
+at the end of the parameter table.  As described in the section
+:ref:`orientation`, the individual $(q_x, q_y)$ points on the detector will
+be rotated into $(q_a, q_b, q_c)$ points relative to the sample in its
+canonical orientation with $a$-$b$-$c$ aligned with $x$-$y$-$z$ in the
+laboratory frame and beam travelling along $-z$.
+
+The oriented C model is called using *Iqabc(qa, qb, qc, par1, par2, ...)* where
+*par1*, etc. are the parameters to the model.  If the shape is rotationally
+symmetric about *c* then *psi* is not needed, and the model is called
+as *Iqac(qab, qc, par1, par2, ...)*.  In either case, the orientation
+parameters are not included in the function call.
+
+For 1D oriented shapes, an integral over all angles is usually needed for
+the *Iq* function. Given symmetry and the substitution $u = \cos(\alpha)$,
+$du = -\sin(\alpha)\,d\alpha$ this becomes
+
+.. math::
+
+    I(q) &= \frac{1}{4\pi} \int_{-\pi/2}^{pi/2} \int_{-pi}^{pi}
+            F(q_a, q_b, q_c)^2 \sin(\alpha)\,d\beta\,d\alpha \\
+        &= \frac{8}{4\pi} \int_{0}^{pi/2} \int_{0}^{\pi/2}
+            F^2 \sin(\alpha)\,d\beta\,d\alpha \\
+        &= \frac{8}{4\pi} \int_1^0 \int_{0}^{\pi/2} - F^2 \,d\beta\,du \\
+        &= \frac{8}{4\pi} \int_0^1 \int_{0}^{\pi/2} F^2 \,d\beta\,du
+
+for
+
+.. math::
+
+    q_a &= q \sin(\alpha)\sin(\beta) = q \sqrt{1-u^2} \sin(\beta) \\
+    q_b &= q \sin(\alpha)\cos(\beta) = q \sqrt{1-u^2} \cos(\beta) \\
+    q_c &= q \cos(\alpha) = q u
+
+Using the $z, w$ values for Gauss-Legendre integration in "lib/gauss76.c", the
+numerical integration is then::
+
+    double outer_sum = 0.0;
+    for (int i = 0; i < GAUSS_N; i++) {
+        const double cos_alpha = 0.5*GAUSS_Z[i] + 0.5;
+        const double sin_alpha = sqrt(1.0 - cos_alpha*cos_alpha);
+        const double qc = cos_alpha * q;
+        double inner_sum = 0.0;
+        for (int j = 0; j < GAUSS_N; j++) {
+            const double beta = M_PI_4 * GAUSS_Z[j] + M_PI_4;
+            double sin_beta, cos_beta;
+            SINCOS(beta, sin_beta, cos_beta);
+            const double qa = sin_alpha * sin_beta * q;
+            const double qb = sin_alpha * cos_beta * q;
+            const double form = Fq(qa, qb, qc, ...);
+            inner_sum += GAUSS_W[j] * form * form;
+        }
+        outer_sum += GAUSS_W[i] * inner_sum;
+    }
+    outer_sum *= 0.25; // = 8/(4 pi) * outer_sum * (pi/2) / 4
+
+The *z* values for the Gauss-Legendre integration extends from -1 to 1, so
+the double sum of *w[i]w[j]* explains the factor of 4.  Correcting for the
+average *dz[i]dz[j]* gives $(1-0) \cdot (\pi/2-0) = \pi/2$.  The $8/(4 \pi)$
+factor comes from the integral over the quadrant.  With less symmetry (eg.,
+in the bcc and fcc paracrystal models), then an integral over the entire
+sphere may be necessary.
+
+For simpler models which are rotationally symmetric a single integral
+suffices:
+
+.. math::
+
+    I(q) &= \frac{1}{\pi}\int_{-\pi/2}^{\pi/2}
+            F(q_{ab}, q_c)^2 \sin(\alpha)\,d\alpha/\pi \\
+        &= \frac{2}{\pi} \int_0^1 F^2\,du
+
+for
+
+.. math::
+
+    q_{ab} &= q \sin(\alpha) = q \sqrt{1 - u^2} \\
+    q_c &= q \cos(\alpha) = q u
+
+
+with integration loop::
+
+    double sum = 0.0;
+    for (int i = 0; i < GAUSS_N; i++) {
+        const double cos_alpha = 0.5*GAUSS_Z[i] + 0.5;
+        const double sin_alpha = sqrt(1.0 - cos_alpha*cos_alpha);
+        const double qab = sin_alpha * q;
+        const double qc = cos_alpha * q;
+        const double form = Fq(qab, qc, ...);
+        sum += GAUSS_W[j] * form * form;
+    }
+    sum *= 0.5; // = 2/pi * sum * (pi/2) / 2
+
+Magnetism
+.........
+
+Magnetism is supported automatically for all shapes by modifying the
+effective SLD of particle according to the Halpern-Johnson vector
+describing the interaction between neutron spin and magnetic field.  All
+parameters marked as type *sld* in the parameter table are treated as
+possibly magnetic particles with magnitude *M0* and direction
+*mtheta* and *mphi*.  Polarization parameters are also provided
+automatically for magnetic models to set the spin state of the measurement.
+
+For more complicated systems where magnetism is not uniform throughout
+the individual particles, you will need to write your own models.
+You should not mark the nuclear sld as type *sld*, but instead leave
+them unmarked and provide your own magnetism and polarization parameters.
+For 2D measurements you will need $(q_x, q_y)$ values for the measurement
+to compute the proper magnetism and orientation, which you can implement
+using *Iqxy(qx, qy, par1, par2, ...)*.
+
 Special Functions
 .................
@@ -543,8 +659,8 @@
     M_PI, M_PI_2, M_PI_4, M_SQRT1_2, M_E:
         $\pi$, $\pi/2$, $\pi/4$, $1/\sqrt{2}$ and Euler's constant $e$
-    exp, log, pow(x,y), expm1, sqrt:
-        Power functions $e^x$, $\ln x$, $x^y$, $e^x - 1$, $\sqrt{x}$.
-        The function expm1(x) is accurate across all $x$, including $x$
-        very close to zero.
+    exp, log, pow(x,y), expm1, log1p, sqrt, cbrt:
+        Power functions $e^x$, $\ln x$, $x^y$, $e^x - 1$, $\ln 1 + x$,
+        $\sqrt{x}$, $\sqrt[3]{x}$. The functions expm1(x) and log1p(x)
+        are accurate across all $x$, including $x$ very close to zero.
     sin, cos, tan, asin, acos, atan:
         Trigonometry functions and inverses, operating on radians.
@@ -557,10 +673,11 @@
         atan(y/x) would return a value in quadrant I. Similarly for
         quadrants II and IV when $x$ and $y$ have opposite sign.
-    fmin(x,y), fmax(x,y), trunc, rint:
+    fabs(x), fmin(x,y), fmax(x,y), trunc, rint:
         Floating point functions.  rint(x) returns the nearest integer.
     NAN:
         NaN, Not a Number, $0/0$.  Use isnan(x) to test for NaN.  Note that
         you cannot use :code:`x == NAN` to test for NaN values since that
-        will always return false.  NAN does not equal NAN!
+        will always return false.  NAN does not equal NAN!  The alternative,
+        :code:`x != x` may fail if the compiler optimizes the test away.
     INFINITY:
         $\infty, 1/0$.  Use isinf(x) to test for infinity, or isfinite(x)
@@ -734,4 +851,7 @@
         Similar arrays are available in :code:`gauss20.c` for 20-point
         quadrature and in :code:`gauss150.c` for 150-point quadrature.
+        The macros :code:`GAUSS_N`, :code:`GAUSS_Z` and :code:`GAUSS_W` are
+        defined so that you can change the order of the integration by
+        selecting an different source without touching the C code.
 
         :code:`source = ["lib/gauss76.c", ...]`
@@ -791,13 +911,4 @@
 can be a model that runs 1000x faster on a good card.  Even your laptop may
 show a 50x improvement or more over the equivalent pure python model.
-
-External C Models
-.................
-
-External C models are very much like embedded C models, except that
-*Iq*, *Iqxy* and *form_volume* are defined in an external source file
-loaded using the *source=[...]* statement. You need to supply the function
-declarations for each of these that you need instead of building them
-automatically from the parameter table.
 
 
@@ -1002,5 +1113,5 @@
 variable name *Rg* for example because $R_g$ is the right name for the model
 parameter then ignore the lint errors.  Also, ignore *missing-docstring*
-for standard model functions *Iq*, *Iqxy*, etc.
+for standard model functions *Iq*, *Iqac*, etc.
 
 We will have delinting sessions at the SasView Code Camps, where we can
Index: explore/asymint.py
===================================================================
--- explore/asymint.py	(revision 1820208ff3a9fba87725a7037153e1cf974a72a9)
+++ explore/asymint.py	(revision a1c32c237d9d08ea8ba721dbaf09ecf6b862c812)
@@ -86,11 +86,44 @@
     a, b, c = env.mpf(a), env.mpf(b), env.mpf(c)
     def Fq(qa, qb, qc):
-        siA = env.sas_sinx_x(0.5*a*qa/2)
-        siB = env.sas_sinx_x(0.5*b*qb/2)
-        siC = env.sas_sinx_x(0.5*c*qc/2)
+        siA = env.sas_sinx_x(a*qa/2)
+        siB = env.sas_sinx_x(b*qb/2)
+        siC = env.sas_sinx_x(c*qc/2)
         return siA * siB * siC
     Fq.__doc__ = "parallelepiped a=%g, b=%g c=%g"%(a, b, c)
     volume = a*b*c
     norm = CONTRAST**2*volume/10000
+    return norm, Fq
+
+def make_core_shell_parallelepiped(a, b, c, da, db, dc, slda, sldb, sldc, env=NPenv):
+    overlapping = False
+    a, b, c = env.mpf(a), env.mpf(b), env.mpf(c)
+    da, db, dc = env.mpf(da), env.mpf(db), env.mpf(dc)
+    slda, sldb, sldc = env.mpf(slda), env.mpf(sldb), env.mpf(sldc)
+    dr0 = CONTRAST
+    drA, drB, drC = slda-SLD_SOLVENT, sldb-SLD_SOLVENT, sldc-SLD_SOLVENT
+    tA, tB, tC = a + 2*da, b + 2*db, c + 2*dc
+    def Fq(qa, qb, qc):
+        siA = a*env.sas_sinx_x(a*qa/2)
+        siB = b*env.sas_sinx_x(b*qb/2)
+        siC = c*env.sas_sinx_x(c*qc/2)
+        siAt = tA*env.sas_sinx_x(tA*qa/2)
+        siBt = tB*env.sas_sinx_x(tB*qb/2)
+        siCt = tC*env.sas_sinx_x(tC*qc/2)
+        if overlapping:
+            return (dr0*siA*siB*siC
+                    + drA*(siAt-siA)*siB*siC
+                    + drB*siAt*(siBt-siB)*siC
+                    + drC*siAt*siBt*(siCt-siC))
+        else:
+            return (dr0*siA*siB*siC
+                    + drA*(siAt-siA)*siB*siC
+                    + drB*siA*(siBt-siB)*siC
+                    + drC*siA*siB*(siCt-siC))
+    Fq.__doc__ = "core-shell parallelepiped a=%g, b=%g c=%g"%(a, b, c)
+    if overlapping:
+        volume = a*b*c + 2*da*b*c + 2*tA*db*c + 2*tA*tB*dc
+    else:
+        volume = a*b*c + 2*da*b*c + 2*a*db*c + 2*a*b*dc
+    norm = 1/(volume*10000)
     return norm, Fq
 
@@ -184,4 +217,22 @@
     NORM, KERNEL = make_parallelepiped(A, B, C)
     NORM_MP, KERNEL_MP = make_parallelepiped(A, B, C, env=MPenv)
+elif shape == 'core_shell_parallelepiped':
+    #A, B, C = 4450, 14000, 47
+    #A, B, C = 445, 140, 47  # integer for the sake of mpf
+    A, B, C = 6800, 114, 1380
+    DA, DB, DC = 2300, 21, 58
+    SLDA, SLDB, SLDC = "5", "-0.3", "11.5"
+    #A,B,C,DA,DB,DC,SLDA,SLDB,SLDC = 10,20,30,100,200,300,1,2,3
+    #SLD_SOLVENT,CONTRAST = 0, 4
+    if 1: # C shortest
+        B, C = C, B
+        DB, DC = DC, DB
+        SLDB, SLDC = SLDC, SLDB
+    elif 0: # C longest
+        A, C = C, A
+        DA, DC = DC, DA
+        SLDA, SLDC = SLDC, SLDA
+    NORM, KERNEL = make_core_shell_parallelepiped(A, B, C, DA, DB, DC, SLDA, SLDB, SLDC)
+    NORM_MP, KERNEL_MP = make_core_shell_parallelepiped(A, B, C, DA, DB, DC, SLDA, SLDB, SLDC, env=MPenv)
 elif shape == 'paracrystal':
     LATTICE = 'bcc'
@@ -342,9 +393,12 @@
     print("gauss-150", *gauss_quad_2d(Q, n=150))
     print("gauss-500", *gauss_quad_2d(Q, n=500))
+    print("gauss-1025", *gauss_quad_2d(Q, n=1025))
+    print("gauss-2049", *gauss_quad_2d(Q, n=2049))
     #gridded_2d(Q, n=2**8+1)
     gridded_2d(Q, n=2**10+1)
-    #gridded_2d(Q, n=2**13+1)
+    #gridded_2d(Q, n=2**12+1)
     #gridded_2d(Q, n=2**15+1)
-    if shape != 'paracrystal':  # adaptive forms are too slow!
+    if shape not in ('paracrystal', 'core_shell_parallelepiped'):
+        # adaptive forms on models for which the calculations are fast enough
         print("dblquad", *scipy_dblquad_2d(Q))
         print("semi-romberg-100", *semi_romberg_2d(Q, n=100))
Index: plore/guyou.py
===================================================================
--- explore/guyou.py	(revision 9d9fcbd8b087f9dd571a6eefd725819968dfea54)
+++ 	(revision )
@@ -1,375 +1,0 @@
-# Copyright 2013-2016 Mike Bostock
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without modification,
-# are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-#   list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-#   this list of conditions and the following disclaimer in the documentation
-#   and/or other materials provided with the distribution.
-#
-# * Neither the name of the author nor the names of contributors may be used to
-#   endorse or promote products derived from this software without specific prior
-#   written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
-# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-# https://github.com/d3/d3-geo-projection
-# commit fd2886555e46b35163c7b898d43c7d1bcebbba7c 2016-07-02
-#
-# 2017-11-01 Paul Kienzle
-# * converted to python, using degrees rather than radians
-from __future__ import division, print_function
-
-import numpy as np
-from numpy import sqrt, pi, tan, cos, sin, log, exp, arctan2 as atan2, sign, radians, degrees
-
-_ = """
-# mpmath version of special functions
-import mpmath as mp
-sn, cn, dn = [mp.ellipfun(v) for v in 'sn', 'cn', 'dn']
-
-def ellipticJi(u, v, m):
-    z = u+v*1j
-    return sn(z, m), cn(z, m), dn(z, m)
-
-def ellipticJ(u, m):
-    s, c, d = sn(u, m), cn(u, m), dn(u, m)
-    phi = mp.asin(s)
-    return s, c, d, phi
-
-def ellipticFi(phi, psi, m):
-    z = phi + psi*1j
-    return mp.ellipf(z, m)
-
-def ellipticF(phi, m):
-    return mp.ellipf(phi, m)
-"""
-
-# scipy version of special functions
-from scipy.special import ellipj as ellipticJ, ellipkinc as ellipticF
-from numpy import sinh, sign, arctan as atan
-
-def ellipticJi(u, v, m):
-    scalar = np.isscalar(u) and np.isscalar(v) and np.isscalar(m)
-    u, v, m = np.broadcast_arrays(u, v, m)
-    result = np.empty_like([u,u,u], 'D')
-    real = v==0
-    imag = u==0
-    mixed = ~(real|imag)
-    result[:, real] = _ellipticJi_real(u[real], m[real])
-    result[:, imag] = _ellipticJi_imag(v[imag], m[imag])
-    result[:, mixed] = _ellipticJi(u[mixed], v[mixed], m[mixed])
-    return result[0,:] if scalar else result
-
-def _ellipticJi_real(u, m):
-    sn, cn, dn, phi = ellipticJ(u, m)
-    return sn, cn, dn
-
-def _ellipticJi_imag(v, m):
-    sn, cn, dn, phi = ellipticJ(v, 1-m)
-    return 1j*sn/cn, 1/cn, dn/cn
-
-def _ellipticJi(u, v, m):
-    # Ignoring special cases for now.
-    #   u=0: (1j*b[0]/b[1], 1/b[1], b[2]/b[1])
-    #   v=0: (a[0], a[1], a[2])
-    a = ellipticJ(u, m)
-    b = ellipticJ(v, 1 - m)
-    c = b[1]**2 + m * (a[0] * b[0])**2
-    return [
-        (a[0] * b[2] / c) + 1j*(a[1] * a[2] * b[0] * b[1] / c),
-        (a[1] * b[1] / c) + 1j*(-a[0] * a[2] * b[0] * b[2] / c),
-        (a[2] * b[1] * b[2] / c) + 1j*(-m * a[0] * a[1] * b[0] / c),
-    ]
-
-# calculate F(phi+ipsi|m).
-# see Abramowitz and Stegun, 17.4.11.
-def ellipticFi(phi, psi, m):
-    if np.any(phi == 0):
-        scalar = np.isscalar(phi) and np.isscalar(psi) and np.isscalar(m)
-        phi, psi, m = np.broadcast_arrays(phi, psi, m)
-        result = np.empty_like(phi, 'D')
-        index = (phi == 0)
-        result[index] = ellipticF(atan(sinh(abs(phi[index]))), 1-m[index]) * sign(psi[index])
-        result[~index] = ellipticFi(phi[~index], psi[~index], m[~index])
-        return result.reshape(1)[0] if scalar else result
-
-    r = abs(phi)
-    i = abs(psi)
-    sinhpsi2 = sinh(i)**2
-    cscphi2 = 1 / sin(r)**2
-    cotphi2 = 1 / tan(r)**2
-    b = -(cotphi2 + m * (sinhpsi2 * cscphi2) - 1 + m)
-    c = (m - 1) * cotphi2
-    cotlambda2 = (-b + sqrt(b * b - 4 * c)) / 2
-    re = ellipticF(atan(1 / sqrt(cotlambda2)), m) * sign(phi)
-    im = ellipticF(atan(sqrt(np.maximum(0,(cotlambda2 / cotphi2 - 1) / m))), 1 - m) * sign(psi)
-    return re + 1j*im
-
-sqrt2 = sqrt(2)
-
-# [PAK] renamed k_ => cos_u, k => sin_u, k*k => sinsq_u to avoid k,K confusion
-# cos_u = 0.171572875253809902396622551580603842860656249246103853646...
-# sinsq_u = 0.970562748477140585620264690516376942836062504523376878120...
-# K = 3.165103454447431823666270142140819753058976299237578486994...
-def guyou(lam, phi):
-    # [PAK] wrap into [-pi/2, pi/2] radians
-    x, y = np.asarray(lam), np.asarray(phi)
-    xn, x = divmod(x+90, 180)
-    yn, y = divmod(y+90, 180)
-    xn, lam = xn*180, radians(x-90)
-    yn, phi = yn*180, radians(y-90)
-
-    # Compute constant K
-    cos_u = (sqrt2 - 1) / (sqrt2 + 1)
-    sinsq_u = 1 - cos_u**2
-    K = ellipticF(pi/2, sinsq_u)
-
-    # [PAK] simplify expressions, using the fact that f = -1
-    # Note: exp(f log(x)) => 1/x,  cos(f x) => cos(x), sin(f x) => -sin(x)
-    r = 1/(tan(pi/4 + abs(phi)/2) * sqrt(cos_u))
-    at = atan(r * (cos(lam) - 1j*sin(lam)))
-    t = ellipticFi(at.real, at.imag, sinsq_u)
-    x, y = (-t.imag, sign(phi + (phi==0))*(0.5 * K - t.real))
-
-    # [PAK] convert to degrees, and return to original tile
-    return degrees(x)+xn, degrees(y)+yn
-
-def guyou_invert(x, y):
-    # [PAK] wrap into [-pi/2, pi/2] radians
-    x, y = np.asarray(x), np.asarray(y)
-    xn, x = divmod(x+90, 180)
-    yn, y = divmod(y+90, 180)
-    xn, x = xn*180, radians(x-90)
-    yn, y = yn*180, radians(y-90)
-
-    # compute constant K
-    cos_u = (sqrt2 - 1) / (sqrt2 + 1)
-    sinsq_u = 1 - cos_u**2
-    K = ellipticF(pi/2, sinsq_u)
-
-    # [PAK] simplify expressions, using the fact that f = -1
-    j = ellipticJi(K/2 - y, -x, sinsq_u)
-    tn = j[0]/j[1]  # j[0], j[1] are complex
-    # Note: -atan2(im(x)/re(x)) => angle(x)
-    lam = -np.angle(tn)
-    # Note: exp(0.5/f log(a re(x)^2 + a im(x)^2)) => 1/(sqrt(a) |x|)
-    phi = 2*atan(1/sqrt(cos_u)/abs(tn)) - pi/2
-
-    # [PAK] convert to degrees, and return to original tile
-    return degrees(lam)+xn, degrees(phi)+yn
-
-def plot_grid():
-    import matplotlib.pyplot as plt
-    from numpy import linspace
-    lat_line = linspace(-90, 90, 400)
-    long_line = linspace(-90, 90, 400)
-
-    #scale = 1
-    limit, step, scale = 90, 10, 2
-    plt.subplot(211)
-    for lat in range(-limit, limit+1, step):
-        x, y = guyou(scale*lat_line, scale*lat)
-        plt.plot(x, y, 'g')
-
-    for long in range(-limit, limit+1, step):
-        x, y = guyou(scale*long, scale*long_line)
-        plt.plot(x, y, 'b')
-    #plt.xlabel('longitude')
-    plt.ylabel('latitude')
-
-    plt.subplot(212)
-    for lat in range(-limit, limit+1, step):
-        x, y = guyou_invert(scale*lat_line, scale*lat)
-        plt.plot(x, y, 'g')
-
-    for long in range(-limit, limit+1, step):
-        x, y = guyou_invert(scale*long, scale*long_line)
-        plt.plot(x, y, 'b')
-    plt.xlabel('longitude')
-    plt.ylabel('latitude')
-
-if __name__ == "__main__":
-    plot_grid()
-    import matplotlib.pyplot as plt; plt.show()
-
-
-_ = """
-// Javascript source for elliptic functions
-//
-// Returns [sn, cn, dn](u + iv|m).
-export function ellipticJi(u, v, m) {
-  var a, b, c;
-  if (!u) {
-    b = ellipticJ(v, 1 - m);
-    return [
-      [0, b[0] / b[1]],
-      [1 / b[1], 0],
-      [b[2] / b[1], 0]
-    ];
-  }
-  a = ellipticJ(u, m);
-  if (!v) return [[a[0], 0], [a[1], 0], [a[2], 0]];
-  b = ellipticJ(v, 1 - m);
-  c = b[1] * b[1] + m * a[0] * a[0] * b[0] * b[0];
-  return [
-    [a[0] * b[2] / c, a[1] * a[2] * b[0] * b[1] / c],
-    [a[1] * b[1] / c, -a[0] * a[2] * b[0] * b[2] / c],
-    [a[2] * b[1] * b[2] / c, -m * a[0] * a[1] * b[0] / c]
-  ];
-}
-
-// Returns [sn, cn, dn, ph](u|m).
-export function ellipticJ(u, m) {
-  var ai, b, phi, t, twon;
-  if (m < epsilon) {
-    t = sin(u);
-    b = cos(u);
-    ai = m * (u - t * b) / 4;
-    return [
-      t - ai * b,
-      b + ai * t,
-      1 - m * t * t / 2,
-      u - ai
-    ];
-  }
-  if (m >= 1 - epsilon) {
-    ai = (1 - m) / 4;
-    b = cosh(u);
-    t = tanh(u);
-    phi = 1 / b;
-    twon = b * sinh(u);
-    return [
-      t + ai * (twon - u) / (b * b),
-      phi - ai * t * phi * (twon - u),
-      phi + ai * t * phi * (twon + u),
-      2 * atan(exp(u)) - halfPi + ai * (twon - u) / b
-    ];
-  }
-
-  var a = [1, 0, 0, 0, 0, 0, 0, 0, 0],
-      c = [sqrt(m), 0, 0, 0, 0, 0, 0, 0, 0],
-      i = 0;
-  b = sqrt(1 - m);
-  twon = 1;
-
-  while (abs(c[i] / a[i]) > epsilon && i < 8) {
-    ai = a[i++];
-    c[i] = (ai - b) / 2;
-    a[i] = (ai + b) / 2;
-    b = sqrt(ai * b);
-    twon *= 2;
-  }
-
-  phi = twon * a[i] * u;
-  do {
-    t = c[i] * sin(b = phi) / a[i];
-    phi = (asin(t) + phi) / 2;
-  } while (--i);
-
-  // [PAK] Cephes uses dn = sqrt(1 - m*sin^2 phi) rather than cos(phi)/cos(phi-b)
-  // DLMF says the second version is unstable near x = K.
-  return [sin(phi), t = cos(phi), t / cos(phi - b), phi];
-}
-
-// calculate F(phi+ipsi|m).
-// see Abramowitz and Stegun, 17.4.11.
-export function ellipticFi(phi, psi, m) {
-  var r = abs(phi),
-      i = abs(psi),
-      sinhpsi = sinh(i);
-  if (r) {
-    var cscphi = 1 / sin(r),
-        cotphi2 = 1 / (tan(r) * tan(r)),
-        b = -(cotphi2 + m * (sinhpsi * sinhpsi * cscphi * cscphi) - 1 + m),
-        c = (m - 1) * cotphi2,
-        cotlambda2 = (-b + sqrt(b * b - 4 * c)) / 2;
-    return [
-      ellipticF(atan(1 / sqrt(cotlambda2)), m) * sign(phi),
-      ellipticF(atan(sqrt((cotlambda2 / cotphi2 - 1) / m)), 1 - m) * sign(psi)
-    ];
-  }
-  return [
-    0,
-    ellipticF(atan(sinhpsi), 1 - m) * sign(psi)
-  ];
-}
-
-// Calculate F(phi|m) where m = k^2 = sin(alpha)^2.
-// See Abramowitz and Stegun, 17.6.7.
-export function ellipticF(phi, m) {
-  if (!m) return phi;
-  if (m === 1) return log(tan(phi / 2 + quarterPi));
-  var a = 1,
-      b = sqrt(1 - m),
-      c = sqrt(m);
-  for (var i = 0; abs(c) > epsilon; i++) {
-    if (phi % pi) {
-      var dPhi = atan(b * tan(phi) / a);
-      if (dPhi < 0) dPhi += pi;
-      phi += dPhi + ~~(phi / pi) * pi;
-    } else phi += phi;
-    c = (a + b) / 2;
-    b = sqrt(a * b);
-    c = ((a = c) - b) / 2;
-  }
-  return phi / (pow(2, i) * a);
-
-
-export function guyouRaw(lambda, phi) {
-  var k_ = (sqrt2 - 1) / (sqrt2 + 1),
-      k = sqrt(1 - k_ * k_),
-      K = ellipticF(halfPi, k * k),
-      f = -1,
-      psi = log(tan(pi / 4 + abs(phi) / 2)),
-      r = exp(f * psi) / sqrt(k_),
-      at = guyouComplexAtan(r * cos(f * lambda), r * sin(f * lambda)),
-      t = ellipticFi(at[0], at[1], k * k);
-  return [-t[1], (phi >= 0 ? 1 : -1) * (0.5 * K - t[0])];
-}
-
-function guyouComplexAtan(x, y) {
-  var x2 = x * x,
-      y_1 = y + 1,
-      t = 1 - x2 - y * y;
-  return [
-   0.5 * ((x >= 0 ? halfPi : -halfPi) - atan2(t, 2 * x)),
-    -0.25 * log(t * t + 4 * x2) +0.5 * log(y_1 * y_1 + x2)
-  ];
-}
-
-function guyouComplexDivide(a, b) {
-  var denominator = b[0] * b[0] + b[1] * b[1];
-  return [
-    (a[0] * b[0] + a[1] * b[1]) / denominator,
-    (a[1] * b[0] - a[0] * b[1]) / denominator
-  ];
-}
-
-guyouRaw.invert = function(x, y) {
-  var k_ = (sqrt2 - 1) / (sqrt2 + 1),
-      k = sqrt(1 - k_ * k_),
-      K = ellipticF(halfPi, k * k),
-      f = -1,
-      j = ellipticJi(0.5 * K - y, -x, k * k),
-      tn = guyouComplexDivide(j[0], j[1]),
-      lambda = atan2(tn[1], tn[0]) / f;
-  return [
-    lambda,
-    2 * atan(exp(0.5 / f * log(k_ * tn[0] * tn[0] + k_ * tn[1] * tn[1]))) - halfPi
-  ];
-};
-"""
Index: plore/jitter.py
===================================================================
--- explore/jitter.py	(revision ff104792a8c7664b8c2b98af11a2db58f784b8d1)
+++ 	(revision )
@@ -1,715 +1,0 @@
-#!/usr/bin/env python
-"""
-Application to explore the difference between sasview 3.x orientation
-dispersity and possible replacement algorithms.
-"""
-from __future__ import division, print_function
-
-import sys, os
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
-
-import argparse
-
-import mpl_toolkits.mplot3d   # Adds projection='3d' option to subplot
-import matplotlib.pyplot as plt
-from matplotlib.widgets import Slider, CheckButtons
-from matplotlib import cm
-import numpy as np
-from numpy import pi, cos, sin, sqrt, exp, degrees, radians
-
-def draw_beam(ax, view=(0, 0)):
-    """
-    Draw the beam going from source at (0, 0, 1) to detector at (0, 0, -1)
-    """
-    #ax.plot([0,0],[0,0],[1,-1])
-    #ax.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]
-
-    ax.plot_surface(x, y, z, rstride=4, cstride=4, color='y', alpha=0.5)
-
-def draw_jitter(ax, view, jitter, dist='gaussian', size=(0.1, 0.4, 1.0)):
-    """
-    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)
-    draw_shape = draw_parallelepiped
-    #draw_shape = draw_ellipsoid
-
-    #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(ax, 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(ax, size, view, delta, alpha=0.8)
-    for v in 'xyz':
-        a, b, c = size
-        lim = np.sqrt(a**2+b**2+c**2)
-        getattr(ax, 'set_'+v+'lim')([-lim, lim])
-        getattr(ax, v+'axis').label.set_text(v)
-
-def draw_ellipsoid(ax, 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)
-
-    ax.plot_surface(x, y, z, rstride=4, cstride=4, color='w', alpha=alpha)
-
-    draw_labels(ax, 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]),
-    ])
-
-def draw_parallelepiped(ax, 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)
-    ax.plot_trisurf(x, y, triangles=tri, Z=z, color='w', alpha=alpha)
-
-    draw_labels(ax, 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]),
-    ])
-
-def draw_sphere(ax, 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))
-    ax.plot_surface(x, y, z, rstride=4, cstride=4, color='w')
-
-PROJECTIONS = [
-    # in order of PROJECTION number; do not change without updating the
-    # constants in kernel_iq.c
-    'equirectangular', 'sinusoidal', 'guyou', 'azimuthal_equidistance',
-]
-def draw_mesh(ax, 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.
-    """
-    theta, phi, psi = view
-    dtheta, dphi, dpsi = jitter
-
-    t = np.linspace(-1, 1, n)
-    weights = np.ones_like(t)
-    if dist == 'gaussian':
-        t *= 3
-        weights = exp(-0.5*t**2)
-    elif dist == 'rectangle':
-        # Note: uses sasmodels ridiculous definition of rectangle width
-        t *= 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, wi, wj):
-            return wi*wj*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, wi, wj):
-            latitude = theta_i
-            scale = cos(radians(latitude))
-            w = 1 if abs(phi_j) < abs(scale)*180 else 0
-            return w*wi*wj
-    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, wi, wj):
-            return wi*wj
-    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, wi, wj):
-            # 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)
-            w = sin(radians(latitude))/latitude if latitude != 0 else 1
-            return w*wi*wj if latitude < 180 else 0
-    elif projection == 'azimuthal_equal_area':
-        def rotate(theta_i, phi_j):
-            R = min(1, sqrt(theta_i**2 + phi_j**2)/180)
-            latitude = 180-degrees(2*np.arccos(R))
-            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, wi, wj):
-            latitude = sqrt(theta_i**2 + phi_j**2)
-            w = sin(radians(latitude))/latitude if latitude != 0 else 1
-            return w*wi*wj if latitude < 180 else 0
-    else:
-        raise ValueError("unknown projection %r"%projection)
-
-    # mesh in theta, phi formed by rotating z
-    z = np.matrix([[0], [0], [radius]])
-    points = np.hstack([rotate(theta_i, phi_j)*z
-                        for theta_i in dtheta*t
-                        for phi_j in dphi*t])
-    # select just the active points (i.e., those with phi < 180
-    w = np.array([weight(theta_i, phi_j, wi, wj)
-                  for wi, theta_i in zip(weights, dtheta*t)
-                  for wj, phi_j in zip(weights, dphi*t)])
-    #print(max(w), min(w), min(w[w>0]))
-    points = points[:, w>0]
-    w = w[w>0]
-    w /= max(w)
-
-    if 0: # Kent distribution
-        points = np.hstack([Rx(phi_j)*Ry(theta_i)*z for theta_i in 30*t for phi_j in 60*t])
-        xp, yp, zp = [np.array(v).flatten() for v in points]
-        kappa = max(1e6, radians(dtheta)/(2*pi))
-        beta = 1/max(1e-6, radians(dphi)/(2*pi))/kappa
-        w = exp(kappa*zp) #+ beta*(xp**2 + yp**2)
-        print(kappa, dtheta, radians(dtheta), min(w), max(w), sum(w))
-        #w /= abs(cos(radians(
-        #w /= sum(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))
-    ax.scatter(x, y, z, c=w, marker='o', vmin=0., vmax=1.)
-
-def draw_labels(ax, 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()
-        ax.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
-def Rx(angle):
-    """Construct a matrix to rotate points about *x* by *angle* degrees."""
-    a = radians(angle)
-    R = [[1, 0, 0],
-         [0, +cos(a), -sin(a)],
-         [0, +sin(a), +cos(a)]]
-    return np.matrix(R)
-
-def Ry(angle):
-    """Construct a matrix to rotate points about *y* by *angle* degrees."""
-    a = radians(angle)
-    R = [[+cos(a), 0, +sin(a)],
-         [0, 1, 0],
-         [-sin(a), 0, +cos(a)]]
-    return np.matrix(R)
-
-def Rz(angle):
-    """Construct a matrix to rotate points about *z* by *angle* degrees."""
-    a = radians(angle)
-    R = [[+cos(a), -sin(a), 0],
-         [+sin(a), +cos(a), 0],
-         [0, 0, 1]]
-    return np.matrix(R)
-
-def transform_xyz(view, jitter, x, y, z):
-    """
-    Send a set of (x,y,z) points through the jitter and view transforms.
-    """
-    x, y, z = [np.asarray(v) for v in (x, y, z)]
-    shape = x.shape
-    points = np.matrix([x.flatten(),y.flatten(),z.flatten()])
-    points = apply_jitter(jitter, points)
-    points = orient_relative_to_beam(view, points)
-    x, y, z = [np.array(v).reshape(shape) for v in points]
-    return x, y, z
-
-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
-
-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
-}
-
-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]
-
-def draw_scattering(calculator, ax, view, jitter, dist='gaussian'):
-    """
-    Plot the scattering for the particular view.
-
-    *calculator* is returned from :func:`build_model`.  *ax* 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)
-        ax.plot_surface(qx, qy, -1.1, rstride=1, cstride=1, facecolors=colors)
-    elif 1:
-        ax.contourf(qx/qx.max(), qy/qy.max(), Iqxy, zdir='z', offset=-1.1,
-                    levels=np.linspace(vmin, vmax, 24))
-    else:
-        ax.pcolormesh(qx, qy, Iqxy)
-
-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
-    from sasmodels.data import empty_data2D
-    from sasmodels.direct_model import DirectModel
-
-    model_info = load_model_info(model_name)
-    model = build_model(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
-
-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
-    if model_name == 'sphere':
-        calculator = build_model('sphere', n=n, radius=c)
-        a = b = c
-    elif model_name == 'bcc_paracrystal':
-        calculator = build_model('bcc_paracrystal', n=n, dnn=c,
-                                  d_factor=0.06, radius=40)
-        a = b = c
-    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', 'triaxial_ellipsoid', 'bcc_paracrystal',
-    'cylinder', 'ellipsoid',
-    'sphere',
- ]
-
-DISTRIBUTIONS = [
-    'gaussian', 'rectangle', 'uniform',
-]
-DIST_LIMITS = {
-    'gaussian': 30,
-    'rectangle': 90/sqrt(3),
-    'uniform': 90,
-}
-
-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 the models available in :func:`select_model`.
-    """
-    # 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)
-
-    ## 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])
-    #ax = plt.subplot(gs[0], projection='3d')
-    ax = plt.axes([0.0, 0.2, 1.0, 0.8], projection='3d')
-    try:  # CRUFT: not all versions of matplotlib accept 'square' 3d projection
-        ax.axis('square')
-    except Exception:
-        pass
-
-    axcolor = 'lightgoldenrodyellow'
-
-    ## add control widgets to plot
-    axtheta  = plt.axes([0.1, 0.15, 0.45, 0.04], axisbg=axcolor)
-    axphi = plt.axes([0.1, 0.1, 0.45, 0.04], axisbg=axcolor)
-    axpsi = plt.axes([0.1, 0.05, 0.45, 0.04], axisbg=axcolor)
-    stheta = Slider(axtheta, 'Theta', -90, 90, valinit=theta)
-    sphi = Slider(axphi, 'Phi', -180, 180, valinit=phi)
-    spsi = Slider(axpsi, 'Psi', -180, 180, valinit=psi)
-
-    axdtheta  = plt.axes([0.75, 0.15, 0.15, 0.04], axisbg=axcolor)
-    axdphi = plt.axes([0.75, 0.1, 0.15, 0.04], axisbg=axcolor)
-    axdpsi= 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(axdtheta, 'dTheta', 0, 2*dlimit, valinit=dtheta)
-    sdphi = Slider(axdphi, 'dPhi', 0, 2*dlimit, valinit=dphi)
-    sdpsi = Slider(axdpsi, '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, 0.5, 5][dims]
-        jitter = [0 if v < limit else v for v in jitter]
-        ax.cla()
-        draw_beam(ax, (0, 0))
-        draw_jitter(ax, view, jitter, dist=dist, size=size)
-        #draw_jitter(ax, view, (0,0,0))
-        draw_mesh(ax, view, jitter, dist=dist, n=mesh, projection=projection)
-        draw_scattering(calculator, ax, 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()
-
-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()
Index: explore/precision.py
===================================================================
--- explore/precision.py	(revision 2a602c705ae8385314cccd395073b6945524dbb0)
+++ explore/precision.py	(revision 2a7e20e9c4ba72da5c6088e743a64f3345ee554d)
@@ -345,5 +345,5 @@
 )
 add_function(
-    name="(1/2+(1-cos(x))/x^2-sin(x)/x)/x",
+    name="(1/2-sin(x)/x+(1-cos(x))/x^2)/x",
     mp_function=lambda x: (0.5 - mp.sin(x)/x + (1-mp.cos(x))/(x*x))/x,
     np_function=lambda x: (0.5 - np.sin(x)/x + (1-np.cos(x))/(x*x))/x,
@@ -609,5 +609,5 @@
     names = ", ".join(sorted(ALL_FUNCTIONS))
     print("""\
-usage: precision.py [-f/a/r] [-x<range>] name...
+usage: precision.py [-f/a/r] [-x<range>] "name" ...
 where
     -f indicates that the function value should be plotted,
@@ -620,5 +620,5 @@
       zoom indicates linear stepping in [1000, 1010]
       neg indicates linear stepping in [-100.1, 100.1]
-and name is "all [first]" or one of:
+and name is "all" or one of:
     """+names)
     sys.exit(1)
Index: explore/realspace.py
===================================================================
--- explore/realspace.py	(revision 8fb2a944dfdac4468e098855656ee605177840c0)
+++ explore/realspace.py	(revision 8fb2a944dfdac4468e098855656ee605177840c0)
@@ -0,0 +1,648 @@
+from __future__ import division, print_function
+
+import time
+from copy import copy
+
+import numpy as np
+from numpy import pi, radians, sin, cos, sqrt
+from numpy.random import poisson, uniform
+from numpy.polynomial.legendre import leggauss
+from scipy.integrate import simps
+from scipy.special import j1 as J1
+
+# Definition of rotation matrices comes from wikipedia:
+#    https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations
+def Rx(angle):
+    """Construct a matrix to rotate points about *x* by *angle* degrees."""
+    a = radians(angle)
+    R = [[1, 0, 0],
+         [0, +cos(a), -sin(a)],
+         [0, +sin(a), +cos(a)]]
+    return np.matrix(R)
+
+def Ry(angle):
+    """Construct a matrix to rotate points about *y* by *angle* degrees."""
+    a = radians(angle)
+    R = [[+cos(a), 0, +sin(a)],
+         [0, 1, 0],
+         [-sin(a), 0, +cos(a)]]
+    return np.matrix(R)
+
+def Rz(angle):
+    """Construct a matrix to rotate points about *z* by *angle* degrees."""
+    a = radians(angle)
+    R = [[+cos(a), -sin(a), 0],
+         [+sin(a), +cos(a), 0],
+         [0, 0, 1]]
+    return np.matrix(R)
+
+def rotation(theta, phi, psi):
+    """
+    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.
+    """
+    return Rx(phi)*Ry(theta)*Rz(psi)
+
+def apply_view(points, view):
+    """
+    Apply the view transform (theta, phi, psi) to a set of points.
+
+    Points are stored in a 3 x n numpy array.
+
+    View angles are in degrees.
+    """
+    theta, phi, psi = view
+    return np.asarray((Rz(phi)*Ry(theta)*Rz(psi))*np.matrix(points.T)).T
+
+
+def invert_view(qx, qy, view):
+    """
+    Return (qa, qb, qc) for the (theta, phi, psi) view angle at detector
+    pixel (qx, qy).
+
+    View angles are in degrees.
+    """
+    theta, phi, psi = view
+    q = np.vstack((qx.flatten(), qy.flatten(), 0*qx.flatten()))
+    return np.asarray((Rz(-psi)*Ry(-theta)*Rz(-phi))*np.matrix(q))
+
+
+class Shape:
+    rotation = np.matrix([[1., 0, 0], [0, 1, 0], [0, 0, 1]])
+    center = np.array([0., 0., 0.])[:, None]
+    r_max = None
+
+    def volume(self):
+        # type: () -> float
+        raise NotImplementedError()
+
+    def sample(self, density):
+        # type: (float) -> np.ndarray[N], np.ndarray[N, 3]
+        raise NotImplementedError()
+
+    def rotate(self, theta, phi, psi):
+        self.rotation = rotation(theta, phi, psi) * self.rotation
+        return self
+
+    def shift(self, x, y, z):
+        self.center = self.center + np.array([x, y, z])[:, None]
+        return self
+
+    def _adjust(self, points):
+        points = np.asarray(self.rotation * np.matrix(points.T)) + self.center
+        return points.T
+
+    def r_bins(self, q, over_sampling=1, r_step=0.):
+        r_max = min(2 * pi / q[0], self.r_max)
+        if r_step == 0.:
+            r_step = 2 * pi / q[-1] / over_sampling
+        #r_step = 0.01
+        return np.arange(r_step, r_max, r_step)
+
+class Composite(Shape):
+    def __init__(self, shapes, center=(0, 0, 0), orientation=(0, 0, 0)):
+        self.shapes = shapes
+        self.rotate(*orientation)
+        self.shift(*center)
+
+        # Find the worst case distance between any two points amongst a set
+        # of shapes independent of orientation.  This could easily be a
+        # factor of two worse than necessary, e.g., a pair of thin rods
+        # end-to-end vs the same pair side-by-side.
+        distances = [((s1.r_max + s2.r_max)/2
+                      + sqrt(np.sum((s1.center - s2.center)**2)))
+                     for s1 in shapes
+                     for s2 in shapes]
+        self.r_max = max(distances + [s.r_max for s in shapes])
+
+    def volume(self):
+        return sum(shape.volume() for shape in self.shapes)
+
+    def sample(self, density):
+        values, points = zip(*(shape.sample(density) for shape in self.shapes))
+        return np.hstack(values), self._adjust(np.vstack(points))
+
+class Box(Shape):
+    def __init__(self, a, b, c,
+                 value, center=(0, 0, 0), orientation=(0, 0, 0)):
+        self.value = np.asarray(value)
+        self.rotate(*orientation)
+        self.shift(*center)
+        self.a, self.b, self.c = a, b, c
+        self._scale = np.array([a/2, b/2, c/2])[None, :]
+        self.r_max = sqrt(a**2 + b**2 + c**2)
+
+    def volume(self):
+        return self.a*self.b*self.c
+
+    def sample(self, density):
+        num_points = poisson(density*self.a*self.b*self.c)
+        points = self._scale*uniform(-1, 1, size=(num_points, 3))
+        values = self.value.repeat(points.shape[0])
+        return values, self._adjust(points)
+
+class EllipticalCylinder(Shape):
+    def __init__(self, ra, rb, length,
+                 value, center=(0, 0, 0), orientation=(0, 0, 0)):
+        self.value = np.asarray(value)
+        self.rotate(*orientation)
+        self.shift(*center)
+        self.ra, self.rb, self.length = ra, rb, length
+        self._scale = np.array([ra, rb, length/2])[None, :]
+        self.r_max = sqrt(4*max(ra, rb)**2 + length**2)
+
+    def volume(self):
+        return pi*self.ra*self.rb*self.length
+
+    def sample(self, density):
+        # density of the bounding box
+        num_points = poisson(density*4*self.ra*self.rb*self.length)
+        points = uniform(-1, 1, size=(num_points, 3))
+        radius = points[:, 0]**2 + points[:, 1]**2
+        points = self._scale*points[radius <= 1]
+        values = self.value.repeat(points.shape[0])
+        return values, self._adjust(points)
+
+class TriaxialEllipsoid(Shape):
+    def __init__(self, ra, rb, rc,
+                 value, center=(0, 0, 0), orientation=(0, 0, 0)):
+        self.value = np.asarray(value)
+        self.rotate(*orientation)
+        self.shift(*center)
+        self.ra, self.rb, self.rc = ra, rb, rc
+        self._scale = np.array([ra, rb, rc])[None, :]
+        self.r_max = 2*max(ra, rb, rc)
+
+    def volume(self):
+        return 4*pi/3 * self.ra * self.rb * self.rc
+
+    def sample(self, density):
+        # randomly sample from a box of side length 2*r, excluding anything
+        # not in the ellipsoid
+        num_points = poisson(density*8*self.ra*self.rb*self.rc)
+        points = uniform(-1, 1, size=(num_points, 3))
+        radius = np.sum(points**2, axis=1)
+        points = self._scale*points[radius <= 1]
+        values = self.value.repeat(points.shape[0])
+        return values, self._adjust(points)
+
+class Helix(Shape):
+    def __init__(self, helix_radius, helix_pitch, tube_radius, tube_length,
+                 value, center=(0, 0, 0), orientation=(0, 0, 0)):
+        self.value = np.asarray(value)
+        self.rotate(*orientation)
+        self.shift(*center)
+        self.helix_radius, self.helix_pitch = helix_radius, helix_pitch
+        self.tube_radius, self.tube_length = tube_radius, tube_length
+        helix_length = helix_pitch * tube_length/sqrt(helix_radius**2 + helix_pitch**2)
+        self.r_max = sqrt((2*helix_radius + 2*tube_radius)*2
+                          + (helix_length + 2*tube_radius)**2)
+
+    def volume(self):
+        # small tube radius approximation; for larger tubes need to account
+        # for the fact that the inner length is much shorter than the outer
+        # length
+        return pi*self.tube_radius**2*self.tube_length
+
+    def points(self, density):
+        num_points = poisson(density*4*self.tube_radius**2*self.tube_length)
+        points = uniform(-1, 1, size=(num_points, 3))
+        radius = points[:, 0]**2 + points[:, 1]**2
+        points = points[radius <= 1]
+
+        # Based on math stackexchange answer by Jyrki Lahtonen
+        #     https://math.stackexchange.com/a/461637
+        # with helix along z rather than x [so tuples in answer are (z, x, y)]
+        # and with random points in the cross section (p1, p2) rather than
+        # uniform points on the surface (cos u, sin u).
+        a, R = self.tube_radius, self.helix_radius
+        h = self.helix_pitch
+        scale = 1/sqrt(R**2 + h**2)
+        t = points[:, 3] * (self.tube_length * scale/2)
+        cos_t, sin_t = cos(t), sin(t)
+
+        # rx = R*cos_t
+        # ry = R*sin_t
+        # rz = h*t
+        # nx = -a * cos_t * points[:, 1]
+        # ny = -a * sin_t * points[:, 1]
+        # nz = 0
+        # bx = (a * h/scale) * sin_t * points[:, 2]
+        # by = (-a * h/scale) * cos_t * points[:, 2]
+        # bz = a*R/scale
+        # x = rx + nx + bx
+        # y = ry + ny + by
+        # z = rz + nz + bz
+        u, v = (R - a*points[:, 1]), (a * h/scale)*points[:, 2]
+        x = u * cos_t + v * sin_t
+        y = u * sin_t - v * cos_t
+        z = a*R/scale + h * t
+
+        points = np.hstack((x, y, z))
+        values = self.value.repeat(points.shape[0])
+        return values, self._adjust(points)
+
+NUMBA = False
+if NUMBA:
+    from numba import njit
+    @njit("f8[:](f8[:],f8[:],f8[:],f8[:],f8[:],f8[:],f8[:])")
+    def _Iqxy(values, x, y, z, qa, qb, qc):
+        Iq = np.zeros_like(qa)
+        for j in range(len(Iq)):
+            total = 0. + 0j
+            for k in range(len(Iq)):
+                total += values[k]*np.exp(1j*(qa[j]*x[k] + qb[j]*y[k] + qc[j]*z[k]))
+            Iq[j] = abs(total)**2
+        return Iq
+
+def calc_Iqxy(qx, qy, rho, points, volume=1.0, view=(0, 0, 0)):
+    qx, qy = np.broadcast_arrays(qx, qy)
+    qa, qb, qc = invert_view(qx, qy, view)
+    rho, volume = np.broadcast_arrays(rho, volume)
+    values = rho*volume
+    x, y, z = points.T
+
+    # I(q) = |sum V(r) rho(r) e^(1j q.r)|^2 / sum V(r)
+    if NUMBA:
+        Iq = _Iqxy(values, x, y, z, qa.flatten(), qb.flatten(), qc.flatten())
+    else:
+        Iq = [abs(np.sum(values*np.exp(1j*(qa_k*x + qb_k*y + qc_k*z))))**2
+              for qa_k, qb_k, qc_k in zip(qa.flat, qb.flat, qc.flat)]
+    return np.asarray(Iq).reshape(qx.shape) / np.sum(volume)
+
+def _calc_Pr_nonuniform(r, rho, points):
+    # Make Pr a little be bigger than necessary so that only distances
+    # min < d < max end up in Pr
+    n_max = len(r)+1
+    extended_Pr = np.zeros(n_max+1, 'd')
+    # r refers to bin centers; find corresponding bin edges
+    bins = bin_edges(r)
+    t_next = time.clock() + 3
+    for k, rho_k in enumerate(rho[:-1]):
+        distance = sqrt(np.sum((points[k] - points[k+1:])**2, axis=1))
+        weights = rho_k * rho[k+1:]
+        index = np.searchsorted(bins, distance)
+        # Note: indices may be duplicated, so "Pr[index] += w" will not work!!
+        extended_Pr += np.bincount(index, weights, n_max+1)
+        t = time.clock()
+        if t > t_next:
+            t_next = t + 3
+            print("processing %d of %d"%(k, len(rho)-1))
+    Pr = extended_Pr[1:-1]
+    return Pr
+
+def _calc_Pr_uniform(r, rho, points):
+    # Make Pr a little be bigger than necessary so that only distances
+    # min < d < max end up in Pr
+    dr, n_max = r[0], len(r)
+    extended_Pr = np.zeros(n_max+1, 'd')
+    t0 = time.clock()
+    t_next = t0 + 3
+    for k, rho_k in enumerate(rho[:-1]):
+        distances = sqrt(np.sum((points[k] - points[k+1:])**2, axis=1))
+        weights = rho_k * rho[k+1:]
+        index = np.minimum(np.asarray(distances/dr, 'i'), n_max)
+        # Note: indices may be duplicated, so "Pr[index] += w" will not work!!
+        extended_Pr += np.bincount(index, weights, n_max+1)
+        t = time.clock()
+        if t > t_next:
+            t_next = t + 3
+            print("processing %d of %d"%(k, len(rho)-1))
+    #print("time py:", time.clock() - t0)
+    Pr = extended_Pr[:-1]
+    # Make Pr independent of sampling density.  The factor of 2 comes because
+    # we are only accumulating the upper triangular distances.
+    #Pr = Pr * 2 / len(rho)**2
+    return Pr
+
+    # Can get an additional 2x by going to C.  Cuda/OpenCL will allow even
+    # more speedup, though still bounded by the n^2 cose.
+    """
+void pdfcalc(int n, const double *pts, const double *rho,
+	     int nPr, double *Pr, double rstep)
+{
+  int i,j;
+
+  for (i=0; i<n-2; i++) {
+    for (j=i+1; j<=n-1; j++) {
+      const double dxx=pts[3*i]-pts[3*j];
+      const double dyy=pts[3*i+1]-pts[3*j+1];
+      const double dzz=pts[3*i+2]-pts[3*j+2];
+      const double d=sqrt(dxx*dxx+dyy*dyy+dzz*dzz);
+      const int k=rint(d/rstep);
+      if (k < nPr) Pr[k]+=rho[i]*rho[j];
+    }
+  }
+}
+"""
+
+if NUMBA:
+    @njit("f8[:](f8[:], f8[:], f8[:,:])")
+    def _calc_Pr_uniform_jit(r, rho, points):
+        dr = r[0]
+        n_max = len(r)
+        Pr = np.zeros_like(r)
+        for j in range(len(rho) - 1):
+            x, y, z = points[j, 0], points[j, 1], points[j, 2]
+            for k in range(j+1, len(rho)):
+                distance = sqrt((x - points[k, 0])**2
+                                + (y - points[k, 1])**2
+                                + (z - points[k, 2])**2)
+                index = int(distance/dr)
+                if index < n_max:
+                    Pr[index] += rho[j] * rho[k]
+        # Make Pr independent of sampling density.  The factor of 2 comes because
+        # we are only accumulating the upper triangular distances.
+        #Pr = Pr * 2 / len(rho)**2
+        return Pr
+
+
+def calc_Pr(r, rho, points):
+    # P(r) with uniform steps in r is 3x faster; check if we are uniform
+    # before continuing
+    if np.max(np.abs(np.diff(r) - r[0])) > r[0]*0.01:
+        Pr = _calc_Pr_nonuniform(r, rho, points)
+    else:
+        if NUMBA:
+            Pr = _calc_Pr_uniform_jit(r, rho, points)
+        else:
+            Pr = _calc_Pr_uniform(r, rho, points)
+    return Pr / Pr.max()
+
+
+def j0(x):
+    return np.sinc(x/np.pi)
+
+def calc_Iq(q, r, Pr):
+    Iq = np.array([simps(Pr * j0(qk*r), r) for qk in q])
+    #Iq = np.array([np.trapz(Pr * j0(qk*r), r) for qk in q])
+    Iq /= Iq[0]
+    return Iq
+
+# NOTE: copied from sasmodels/resolution.py
+def bin_edges(x):
+    """
+    Determine bin edges from bin centers, assuming that edges are centered
+    between the bins.
+
+    Note: this uses the arithmetic mean, which may not be appropriate for
+    log-scaled data.
+    """
+    if len(x) < 2 or (np.diff(x) < 0).any():
+        raise ValueError("Expected bins to be an increasing set")
+    edges = np.hstack([
+        x[0]  - 0.5*(x[1]  - x[0]),  # first point minus half first interval
+        0.5*(x[1:] + x[:-1]),        # mid points of all central intervals
+        x[-1] + 0.5*(x[-1] - x[-2]), # last point plus half last interval
+        ])
+    return edges
+
+def plot_calc(r, Pr, q, Iq, theory=None):
+    import matplotlib.pyplot as plt
+    plt.subplot(211)
+    plt.plot(r, Pr, '-', label="Pr")
+    plt.xlabel('r (A)')
+    plt.ylabel('Pr (1/A^2)')
+    plt.subplot(212)
+    plt.loglog(q, Iq, '-', label='from Pr')
+    plt.xlabel('q (1/A')
+    plt.ylabel('Iq')
+    if theory is not None:
+        plt.loglog(theory[0], theory[1], '-', label='analytic')
+        plt.legend()
+
+def plot_calc_2d(qx, qy, Iqxy, theory=None):
+    import matplotlib.pyplot as plt
+    qx, qy = bin_edges(qx), bin_edges(qy)
+    #qx, qy = np.meshgrid(qx, qy)
+    if theory is not None:
+        plt.subplot(121)
+    plt.pcolormesh(qx, qy, np.log10(Iqxy))
+    plt.xlabel('qx (1/A)')
+    plt.ylabel('qy (1/A)')
+    if theory is not None:
+        plt.subplot(122)
+        plt.pcolormesh(qx, qy, np.log10(theory))
+        plt.xlabel('qx (1/A)')
+
+def plot_points(rho, points):
+    import mpl_toolkits.mplot3d
+    import matplotlib.pyplot as plt
+
+    ax = plt.axes(projection='3d')
+    try:
+        ax.axis('square')
+    except Exception:
+        pass
+    n = len(points)
+    #print("len points", n)
+    index = np.random.choice(n, size=500) if n > 500 else slice(None, None)
+    ax.scatter(points[index, 0], points[index, 1], points[index, 2], c=rho[index])
+    #low, high = points.min(axis=0), points.max(axis=0)
+    #ax.axis([low[0], high[0], low[1], high[1], low[2], high[2]])
+    ax.autoscale(True)
+
+def check_shape(shape, fn=None):
+    rho_solvent = 0
+    q = np.logspace(-3, 0, 200)
+    r = shape.r_bins(q, r_step=0.01)
+    sampling_density = 6*5000 / shape.volume()
+    rho, points = shape.sample(sampling_density)
+    t0 = time.time()
+    Pr = calc_Pr(r, rho-rho_solvent, points)
+    print("calc Pr time", time.time() - t0)
+    Iq = calc_Iq(q, r, Pr)
+    theory = (q, fn(q)) if fn is not None else None
+
+    import pylab
+    #plot_points(rho, points); pylab.figure()
+    plot_calc(r, Pr, q, Iq, theory=theory)
+    pylab.show()
+
+def check_shape_2d(shape, fn=None, view=(0, 0, 0)):
+    rho_solvent = 0
+    nq, qmax = 100, 1.0
+    qx = np.linspace(0.0, qmax, nq)
+    qy = np.linspace(0.0, qmax, nq)
+    Qx, Qy = np.meshgrid(qx, qy)
+    sampling_density = 50000 / shape.volume()
+    #t0 = time.time()
+    rho, points = shape.sample(sampling_density)
+    #print("sample time", time.time() - t0)
+    t0 = time.time()
+    Iqxy = calc_Iqxy(Qx, Qy, rho, points, view=view)
+    print("calc time", time.time() - t0)
+    theory = fn(Qx, Qy) if fn is not None else None
+    Iqxy += 0.001 * Iqxy.max()
+    if theory is not None:
+        theory += 0.001 * theory.max()
+
+    import pylab
+    #plot_points(rho, points); pylab.figure()
+    plot_calc_2d(qx, qy, Iqxy, theory=theory)
+    pylab.show()
+
+def sas_sinx_x(x):
+    with np.errstate(all='ignore'):
+        retvalue = sin(x)/x
+    retvalue[x == 0.] = 1.
+    return retvalue
+
+def sas_2J1x_x(x):
+    with np.errstate(all='ignore'):
+        retvalue = 2*J1(x)/x
+    retvalue[x == 0] = 1.
+    return retvalue
+
+def sas_3j1x_x(x):
+    """return 3*j1(x)/x"""
+    with np.errstate(all='ignore'):
+        retvalue = 3*(sin(x) - x*cos(x))/x**3
+    retvalue[x == 0.] = 1.
+    return retvalue
+
+def cylinder_Iq(q, radius, length):
+    z, w = leggauss(76)
+    cos_alpha = (z+1)/2
+    sin_alpha = sqrt(1.0 - cos_alpha**2)
+    Iq = np.empty_like(q)
+    for k, qk in enumerate(q):
+        qab, qc = qk*sin_alpha, qk*cos_alpha
+        Fq = sas_2J1x_x(qab*radius) * j0(qc*length/2)
+        Iq[k] = np.sum(w*Fq**2)
+    Iq = Iq/Iq[0]
+    return Iq
+
+def cylinder_Iqxy(qx, qy, radius, length, view=(0, 0, 0)):
+    qa, qb, qc = invert_view(qx, qy, view)
+    qab = np.sqrt(qa**2 + qb**2)
+    Fq = sas_2J1x_x(qab*radius) * j0(qc*length/2)
+    Iq = Fq**2
+    return Iq.reshape(qx.shape)
+
+def sphere_Iq(q, radius):
+    Iq = sas_3j1x_x(q*radius)**2
+    return Iq/Iq[0]
+
+def csbox_Iq(q, a, b, c, da, db, dc, slda, sldb, sldc, sld_core):
+    z, w = leggauss(76)
+
+    sld_solvent = 0
+    overlapping = False
+    dr0 = sld_core - sld_solvent
+    drA, drB, drC = slda-sld_solvent, sldb-sld_solvent, sldc-sld_solvent
+    tA, tB, tC = a + 2*da, b + 2*db, c + 2*dc
+
+    outer_sum = np.zeros_like(q)
+    for cos_alpha, outer_w in zip((z+1)/2, w):
+        sin_alpha = sqrt(1.0-cos_alpha*cos_alpha)
+        qc = q*cos_alpha
+        siC = c*j0(c*qc/2)
+        siCt = tC*j0(tC*qc/2)
+        inner_sum = np.zeros_like(q)
+        for beta, inner_w in zip((z + 1)*pi/4, w):
+            qa, qb = q*sin_alpha*sin(beta), q*sin_alpha*cos(beta)
+            siA = a*j0(a*qa/2)
+            siB = b*j0(b*qb/2)
+            siAt = tA*j0(tA*qa/2)
+            siBt = tB*j0(tB*qb/2)
+            if overlapping:
+                Fq = (dr0*siA*siB*siC
+                      + drA*(siAt-siA)*siB*siC
+                      + drB*siAt*(siBt-siB)*siC
+                      + drC*siAt*siBt*(siCt-siC))
+            else:
+                Fq = (dr0*siA*siB*siC
+                      + drA*(siAt-siA)*siB*siC
+                      + drB*siA*(siBt-siB)*siC
+                      + drC*siA*siB*(siCt-siC))
+            inner_sum += inner_w * Fq**2
+        outer_sum += outer_w * inner_sum
+    Iq = outer_sum / 4  # = outer*um*zm*8.0/(4.0*M_PI)
+    return Iq/Iq[0]
+
+def csbox_Iqxy(qx, qy, a, b, c, da, db, dc, slda, sldb, sldc, sld_core, view=(0,0,0)):
+    qa, qb, qc = invert_view(qx, qy, view)
+
+    sld_solvent = 0
+    overlapping = False
+    dr0 = sld_core - sld_solvent
+    drA, drB, drC = slda-sld_solvent, sldb-sld_solvent, sldc-sld_solvent
+    tA, tB, tC = a + 2*da, b + 2*db, c + 2*dc
+    siA = a*sas_sinx_x(a*qa/2)
+    siB = b*sas_sinx_x(b*qb/2)
+    siC = c*sas_sinx_x(c*qc/2)
+    siAt = tA*sas_sinx_x(tA*qa/2)
+    siBt = tB*sas_sinx_x(tB*qb/2)
+    siCt = tC*sas_sinx_x(tC*qc/2)
+    Fq = (dr0*siA*siB*siC
+          + drA*(siAt-siA)*siB*siC
+          + drB*siA*(siBt-siB)*siC
+          + drC*siA*siB*(siCt-siC))
+    Iq = Fq**2
+    return Iq.reshape(qx.shape)
+
+def check_cylinder(radius=25, length=125, rho=2.):
+    shape = EllipticalCylinder(radius, radius, length, rho)
+    fn = lambda q: cylinder_Iq(q, radius, length)
+    check_shape(shape, fn)
+
+def check_cylinder_2d(radius=25, length=125, rho=2., view=(0, 0, 0)):
+    shape = EllipticalCylinder(radius, radius, length, rho)
+    fn = lambda qx, qy, view=view: cylinder_Iqxy(qx, qy, radius, length, view=view)
+    check_shape_2d(shape, fn, view=view)
+
+def check_cylinder_2d_lattice(radius=25, length=125, rho=2.,
+                              view=(0, 0, 0)):
+    nx, dx = 1, 2*radius
+    ny, dy = 30, 2*radius
+    nz, dz = 30, length
+    dx, dy, dz = 2*dx, 2*dy, 2*dz
+    def center(*args):
+        sigma = 0.333
+        space = 2
+        return [(space*n+np.random.randn()*sigma)*x for n, x in args]
+    shapes = [EllipticalCylinder(radius, radius, length, rho,
+                                 #center=(ix*dx, iy*dy, iz*dz)
+                                 orientation=np.random.randn(3)*0,
+                                 center=center((ix, dx), (iy, dy), (iz, dz))
+                                )
+              for ix in range(nx)
+              for iy in range(ny)
+              for iz in range(nz)]
+    shape = Composite(shapes)
+    fn = lambda qx, qy, view=view: cylinder_Iqxy(qx, qy, radius, length, view=view)
+    check_shape_2d(shape, fn, view=view)
+
+def check_sphere(radius=125, rho=2):
+    shape = TriaxialEllipsoid(radius, radius, radius, rho)
+    fn = lambda q: sphere_Iq(q, radius)
+    check_shape(shape, fn)
+
+def check_csbox(a=10, b=20, c=30, da=1, db=2, dc=3, slda=1, sldb=2, sldc=3, sld_core=4):
+    core = Box(a, b, c, sld_core)
+    side_a = Box(da, b, c, slda, center=((a+da)/2, 0, 0))
+    side_b = Box(a, db, c, sldb, center=(0, (b+db)/2, 0))
+    side_c = Box(a, b, dc, sldc, center=(0, 0, (c+dc)/2))
+    side_a2 = copy(side_a).shift(-a-da, 0, 0)
+    side_b2 = copy(side_b).shift(0, -b-db, 0)
+    side_c2 = copy(side_c).shift(0, 0, -c-dc)
+    shape = Composite((core, side_a, side_b, side_c, side_a2, side_b2, side_c2))
+    def fn(q):
+        return csbox_Iq(q, a, b, c, da, db, dc, slda, sldb, sldc, sld_core)
+    #check_shape(shape, fn)
+
+    view = (20, 30, 40)
+    def fn_xy(qx, qy):
+        return csbox_Iqxy(qx, qy, a, b, c, da, db, dc,
+                          slda, sldb, sldc, sld_core, view=view)
+    check_shape_2d(shape, fn_xy, view=view)
+
+if __name__ == "__main__":
+    check_cylinder(radius=10, length=40)
+    #check_cylinder_2d(radius=10, length=40, view=(90,30,0))
+    #check_cylinder_2d_lattice(radius=10, length=50, view=(90,30,0))
+    #check_sphere()
+    #check_csbox()
+    #check_csbox(da=100, db=200, dc=300)
Index: sasmodels/compare.py
===================================================================
--- sasmodels/compare.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/compare.py	(revision 2a7e20e9c4ba72da5c6088e743a64f3345ee554d)
@@ -42,5 +42,5 @@
 from .data import plot_theory, empty_data1D, empty_data2D, load_data
 from .direct_model import DirectModel, get_mesh
-from .generate import FLOAT_RE
+from .generate import FLOAT_RE, set_integration_size
 from .weights import plot_weights
 
@@ -92,4 +92,5 @@
     -accuracy=Low accuracy of the resolution calculation Low, Mid, High, Xhigh
     -neval=1 sets the number of evals for more accurate timing
+    -ngauss=0 overrides the number of points in the 1-D gaussian quadrature
 
     === precision options ===
@@ -693,5 +694,5 @@
         data = empty_data2D(q, resolution=res)
         data.accuracy = opts['accuracy']
-        set_beam_stop(data, 0.0004)
+        set_beam_stop(data, qmin)
         index = ~data.mask
     else:
@@ -706,5 +707,5 @@
     return data, index
 
-def make_engine(model_info, data, dtype, cutoff):
+def make_engine(model_info, data, dtype, cutoff, ngauss=0):
     # type: (ModelInfo, Data, str, float) -> Calculator
     """
@@ -714,4 +715,7 @@
     than OpenCL.
     """
+    if ngauss:
+        set_integration_size(model_info, ngauss)
+
     if dtype is None or not dtype.endswith('!'):
         return eval_opencl(model_info, data, dtype=dtype, cutoff=cutoff)
@@ -954,5 +958,5 @@
     'poly', 'mono', 'cutoff=',
     'magnetic', 'nonmagnetic',
-    'accuracy=',
+    'accuracy=', 'ngauss=',
     'neval=',  # for timing...
 
@@ -1089,4 +1093,5 @@
         'show_weights' : False,
         'sphere'    : 0,
+        'ngauss'    : '0',
     }
     for arg in flags:
@@ -1115,4 +1120,5 @@
         elif arg.startswith('-engine='):   opts['engine'] = arg[8:]
         elif arg.startswith('-neval='):    opts['count'] = arg[7:]
+        elif arg.startswith('-ngauss='):   opts['ngauss'] = arg[8:]
         elif arg.startswith('-random='):
             opts['seed'] = int(arg[8:])
@@ -1169,4 +1175,5 @@
 
     comparison = any(PAR_SPLIT in v for v in values)
+
     if PAR_SPLIT in name:
         names = name.split(PAR_SPLIT, 2)
@@ -1181,4 +1188,10 @@
         return None
 
+    if PAR_SPLIT in opts['ngauss']:
+        opts['ngauss'] = [int(k) for k in opts['ngauss'].split(PAR_SPLIT, 2)]
+        comparison = True
+    else:
+        opts['ngauss'] = [int(opts['ngauss'])]*2
+
     if PAR_SPLIT in opts['engine']:
         opts['engine'] = opts['engine'].split(PAR_SPLIT, 2)
@@ -1199,7 +1212,9 @@
         opts['cutoff'] = [float(opts['cutoff'])]*2
 
-    base = make_engine(model_info[0], data, opts['engine'][0], opts['cutoff'][0])
+    base = make_engine(model_info[0], data, opts['engine'][0],
+                       opts['cutoff'][0], opts['ngauss'][0])
     if comparison:
-        comp = make_engine(model_info[1], data, opts['engine'][1], opts['cutoff'][1])
+        comp = make_engine(model_info[1], data, opts['engine'][1],
+                           opts['cutoff'][1], opts['ngauss'][1])
     else:
         comp = None
@@ -1274,5 +1289,5 @@
         if model_info != model_info2:
             pars2 = randomize_pars(model_info2, pars2)
-            limit_dimensions(model_info, pars2, maxdim)
+            limit_dimensions(model_info2, pars2, maxdim)
             # Share values for parameters with the same name
             for k, v in pars.items():
Index: sasmodels/details.py
===================================================================
--- sasmodels/details.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/details.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -258,4 +258,7 @@
     # type: (...) -> Sequence[np.ndarray]
     """
+    **Deprecated** Theta weights will be computed in the kernel wrapper if
+    they are needed.
+
     If there is a theta parameter, update the weights of that parameter so that
     the cosine weighting required for polar integration is preserved.
@@ -272,5 +275,4 @@
     Returns updated weights vectors
     """
-    # TODO: explain in a comment why scale and background are missing
     # Apparently the parameters.theta_offset similarly skips scale and
     # and background, so the indexing works out, but they are still shipped
@@ -279,5 +281,4 @@
         index = parameters.theta_offset
         theta = dispersity[index]
-        # TODO: modify the dispersity vector to avoid the theta=-90,90,270,...
         theta_weight = abs(cos(radians(theta)))
         weights = tuple(theta_weight*w if k == index else w
Index: sasmodels/generate.py
===================================================================
--- sasmodels/generate.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/generate.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -7,10 +7,17 @@
     particular dimensions averaged over all orientations.
 
-    *Iqxy(qx, qy, p1, p2, ...)* returns the scattering at qx, qy for a form
-    with particular dimensions for a single orientation.
-
-    *Imagnetic(qx, qy, result[], p1, p2, ...)* returns the scattering for the
-    polarized neutron spin states (up-up, up-down, down-up, down-down) for
-    a form with particular dimensions for a single orientation.
+    *Iqac(qab, qc, p1, p2, ...)* returns the scattering at qab, qc
+    for a rotationally symmetric form with particular dimensions.
+    qab, qc are determined from shape orientation and scattering angles.
+    This call is used if the shape has orientation parameters theta and phi.
+
+    *Iqabc(qa, qb, qc, p1, p2, ...)* returns the scattering at qa, qb, qc
+    for a form with particular dimensions.  qa, qb, qc are determined from
+    shape orientation and scattering angles. This call is used if the shape
+    has orientation parameters theta, phi and psi.
+
+    *Iqxy(qx, qy, p1, p2, ...)* returns the scattering at qx, qy.  Use this
+    to create an arbitrary 2D theory function, needed for q-dependent
+    background functions and for models with non-uniform magnetism.
 
     *form_volume(p1, p2, ...)* returns the volume of the form with particular
@@ -31,13 +38,12 @@
 scale and background parameters for each model.
 
-*Iq*, *Iqxy*, *Imagnetic* and *form_volume* should be stylized C-99
-functions written for OpenCL.  All functions need prototype declarations
-even if the are defined before they are used.  OpenCL does not support
-*#include* preprocessor directives, so instead the list of includes needs
-to be given as part of the metadata in the kernel module definition.
-The included files should be listed using a path relative to the kernel
-module, or if using "lib/file.c" if it is one of the standard includes
-provided with the sasmodels source.  The includes need to be listed in
-order so that functions are defined before they are used.
+C code should be stylized C-99 functions written for OpenCL. All functions
+need prototype declarations even if the are defined before they are used.
+Although OpenCL supports *#include* preprocessor directives, the list of
+includes should be given as part of the metadata in the kernel module
+definition. The included files should be listed using a path relative to the
+kernel module, or if using "lib/file.c" if it is one of the standard includes
+provided with the sasmodels source. The includes need to be listed in order
+so that functions are defined before they are used.
 
 Floating point values should be declared as *double*.  For single precision
@@ -107,9 +113,9 @@
     present, the volume ratio is 1.
 
-    *form_volume*, *Iq*, *Iqxy*, *Imagnetic* are strings containing the
-    C source code for the body of the volume, Iq, and Iqxy functions
+    *form_volume*, *Iq*, *Iqac*, *Iqabc* are strings containing
+    the C source code for the body of the volume, Iq, and Iqac functions
     respectively.  These can also be defined in the last source file.
 
-    *Iq* and *Iqxy* also be instead be python functions defining the
+    *Iq*, *Iqac*, *Iqabc* also be instead be python functions defining the
     kernel.  If they are marked as *Iq.vectorized = True* then the
     kernel is passed the entire *q* vector at once, otherwise it is
@@ -168,4 +174,5 @@
 from zlib import crc32
 from inspect import currentframe, getframeinfo
+import logging
 
 import numpy as np  # type: ignore
@@ -181,4 +188,6 @@
     pass
 # pylint: enable=unused-import
+
+logger = logging.getLogger(__name__)
 
 # jitter projection to use in the kernel code.  See explore/jitter.py
@@ -270,4 +279,22 @@
 """
 
+
+def set_integration_size(info, n):
+    # type: (ModelInfo, int) -> None
+    """
+    Update the model definition, replacing the gaussian integration with
+    a gaussian integration of a different size.
+
+    Note: this really ought to be a method in modelinfo, but that leads to
+    import loops.
+    """
+    if (info.source and any(lib.startswith('lib/gauss') for lib in info.source)):
+        import os.path
+        from .gengauss import gengauss
+        path = os.path.join(MODEL_PATH, "lib", "gauss%d.c"%n)
+        if not os.path.exists(path):
+            gengauss(n, path)
+        info.source = ["lib/gauss%d.c"%n if lib.startswith('lib/gauss')
+                        else lib for lib in info.source]
 
 def format_units(units):
@@ -608,6 +635,6 @@
 
 """
-def _gen_fn(name, pars, body, filename, line):
-    # type: (str, List[Parameter], str, str, int) -> str
+def _gen_fn(model_info, name, pars):
+    # type: (ModelInfo, str, List[Parameter]) -> str
     """
     Generate a function given pars and body.
@@ -621,7 +648,11 @@
     """
     par_decl = ', '.join(p.as_function_argument() for p in pars) if pars else 'void'
+    body = getattr(model_info, name)
+    filename = model_info.filename
+    # Note: if symbol is defined strangely in the module then default it to 1
+    lineno = model_info.lineno.get(name, 1)
     return _FN_TEMPLATE % {
         'name': name, 'pars': par_decl, 'body': body,
-        'filename': filename.replace('\\', '\\\\'), 'line': line,
+        'filename': filename.replace('\\', '\\\\'), 'line': lineno,
     }
 
@@ -638,34 +669,40 @@
 
 # type in IQXY pattern could be single, float, double, long double, ...
-_IQXY_PATTERN = re.compile("^((inline|static) )? *([a-z ]+ )? *Iqxy *([(]|$)",
+_IQXY_PATTERN = re.compile(r"(^|\s)double\s+I(?P<mode>q(ab?c|xy))\s*[(]",
                            flags=re.MULTILINE)
-def _have_Iqxy(sources):
+def find_xy_mode(source):
     # type: (List[str]) -> bool
     """
-    Return true if any file defines Iqxy.
+    Return the xy mode as qa, qac, qabc or qxy.
 
     Note this is not a C parser, and so can be easily confused by
     non-standard syntax.  Also, it will incorrectly identify the following
-    as having Iqxy::
+    as having 2D models::
 
         /*
-        double Iqxy(qx, qy, ...) { ... fill this in later ... }
+        double Iqac(qab, qc, ...) { ... fill this in later ... }
         */
 
-    If you want to comment out an Iqxy function, use // on the front of the
-    line instead.
-    """
-    for _path, code in sources:
-        if _IQXY_PATTERN.search(code):
-            return True
-    return False
-
-
-def _add_source(source, code, path):
+    If you want to comment out the function, use // on the front of the
+    line::
+
+        /*
+        // double Iqac(qab, qc, ...) { ... fill this in later ... }
+        */
+
+    """
+    for code in source:
+        m = _IQXY_PATTERN.search(code)
+        if m is not None:
+            return m.group('mode')
+    return 'qa'
+
+
+def _add_source(source, code, path, lineno=1):
     """
     Add a file to the list of source code chunks, tagged with path and line.
     """
     path = path.replace('\\', '\\\\')
-    source.append('#line 1 "%s"' % path)
+    source.append('#line %d "%s"' % (lineno, path))
     source.append(code)
 
@@ -698,9 +735,4 @@
     user_code = [(f, open(f).read()) for f in model_sources(model_info)]
 
-    # What kind of 2D model do we need?
-    xy_mode = ('qa' if not _have_Iqxy(user_code) and not isinstance(model_info.Iqxy, str)
-               else 'qac' if not partable.is_asymmetric
-               else 'qabc')
-
     # Build initial sources
     source = []
@@ -709,19 +741,41 @@
         _add_source(source, code, path)
 
+    if model_info.c_code:
+        _add_source(source, model_info.c_code, model_info.filename,
+                    lineno=model_info.lineno.get('c_code', 1))
+
     # Make parameters for q, qx, qy so that we can use them in declarations
-    q, qx, qy = [Parameter(name=v) for v in ('q', 'qx', 'qy')]
+    q, qx, qy, qab, qa, qb, qc \
+        = [Parameter(name=v) for v in 'q qx qy qab qa qb qc'.split()]
     # Generate form_volume function, etc. from body only
     if isinstance(model_info.form_volume, str):
         pars = partable.form_volume_parameters
-        source.append(_gen_fn('form_volume', pars, model_info.form_volume,
-                              model_info.filename, model_info._form_volume_line))
+        source.append(_gen_fn(model_info, 'form_volume', pars))
     if isinstance(model_info.Iq, str):
         pars = [q] + partable.iq_parameters
-        source.append(_gen_fn('Iq', pars, model_info.Iq,
-                              model_info.filename, model_info._Iq_line))
+        source.append(_gen_fn(model_info, 'Iq', pars))
     if isinstance(model_info.Iqxy, str):
-        pars = [qx, qy] + partable.iqxy_parameters
-        source.append(_gen_fn('Iqxy', pars, model_info.Iqxy,
-                              model_info.filename, model_info._Iqxy_line))
+        pars = [qx, qy] + partable.iq_parameters + partable.orientation_parameters
+        source.append(_gen_fn(model_info, 'Iqxy', pars))
+    if isinstance(model_info.Iqac, str):
+        pars = [qab, qc] + partable.iq_parameters
+        source.append(_gen_fn(model_info, 'Iqac', pars))
+    if isinstance(model_info.Iqabc, str):
+        pars = [qa, qb, qc] + partable.iq_parameters
+        source.append(_gen_fn(model_info, 'Iqabc', pars))
+
+    # What kind of 2D model do we need?  Is it consistent with the parameters?
+    xy_mode = find_xy_mode(source)
+    if xy_mode == 'qabc' and not partable.is_asymmetric:
+        raise ValueError("asymmetric oriented models need to define Iqabc")
+    elif xy_mode == 'qac' and partable.is_asymmetric:
+        raise ValueError("symmetric oriented models need to define Iqac")
+    elif not partable.orientation_parameters and xy_mode in ('qac', 'qabc'):
+        raise ValueError("Unexpected function I%s for unoriented shape"%xy_mode)
+    elif partable.orientation_parameters and xy_mode not in ('qac', 'qabc'):
+        if xy_mode == 'qxy':
+            logger.warn("oriented shapes should define Iqac or Iqabc")
+        else:
+            raise ValueError("Expected function Iqac or Iqabc for oriented shape")
 
     # Define the parameter table
@@ -749,14 +803,26 @@
     if xy_mode == 'qabc':
         pars = ",".join(["_qa", "_qb", "_qc"] + model_refs)
-        call_iqxy = "#define CALL_IQ_ABC(_qa,_qb,_qc,_v) Iqxy(%s)" % pars
+        call_iqxy = "#define CALL_IQ_ABC(_qa,_qb,_qc,_v) Iqabc(%s)" % pars
         clear_iqxy = "#undef CALL_IQ_ABC"
     elif xy_mode == 'qac':
         pars = ",".join(["_qa", "_qc"] + model_refs)
-        call_iqxy = "#define CALL_IQ_AC(_qa,_qc,_v) Iqxy(%s)" % pars
+        call_iqxy = "#define CALL_IQ_AC(_qa,_qc,_v) Iqac(%s)" % pars
         clear_iqxy = "#undef CALL_IQ_AC"
-    else:  # xy_mode == 'qa'
+    elif xy_mode == 'qa':
         pars = ",".join(["_qa"] + model_refs)
         call_iqxy = "#define CALL_IQ_A(_qa,_v) Iq(%s)" % pars
         clear_iqxy = "#undef CALL_IQ_A"
+    elif xy_mode == 'qxy':
+        orientation_refs = _call_pars("_v.", partable.orientation_parameters)
+        pars = ",".join(["_qx", "_qy"] + model_refs + orientation_refs)
+        call_iqxy = "#define CALL_IQ_XY(_qx,_qy,_v) Iqxy(%s)" % pars
+        clear_iqxy = "#undef CALL_IQ_XY"
+        if partable.orientation_parameters:
+            call_iqxy += "\n#define HAVE_THETA"
+            clear_iqxy += "\n#undef HAVE_THETA"
+        if partable.is_asymmetric:
+            call_iqxy += "\n#define HAVE_PSI"
+            clear_iqxy += "\n#undef HAVE_PSI"
+
 
     magpars = [k-2 for k, p in enumerate(partable.call_parameters)
Index: sasmodels/gengauss.py
===================================================================
--- sasmodels/gengauss.py	(revision ff31782cc25cfbfea355acdd6d0204c959b41dcc)
+++ sasmodels/gengauss.py	(revision ff31782cc25cfbfea355acdd6d0204c959b41dcc)
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+from __future__ import division, print_function
+
+import sys
+
+import numpy as np
+from numpy.polynomial.legendre import leggauss
+
+def gengauss(n, path):
+    z, w = leggauss(n)
+
+    # Make sure array size is a multiple of 4
+    if n%4:
+        array_size = n + (4 - n%4)
+        z, w = [np.hstack((v, [0.]*(4-n%4))) for v in (z, w)]
+    else:
+        array_size = n
+
+    with open(path, "w") as fid:
+        fid.write("""\
+// Generated by sasmodels.gengauss.gengauss(%d)
+
+#ifdef GAUSS_N
+# undef GAUSS_N
+# undef GAUSS_Z
+# undef GAUSS_W
+#endif
+#define GAUSS_N %d
+#define GAUSS_Z Gauss%dZ
+#define GAUSS_W Gauss%dWt
+
+"""%(n, n, n, n))
+
+        if array_size != n:
+            fid.write("// Note: using array size %d so that it is a multiple of 4\n\n"%array_size)
+
+        fid.write("constant double Gauss%dWt[%d]={\n"%(n, array_size))
+        fid.write(",\n".join("\t% .15e"%v for v in w))
+        fid.write("\n};\n")
+
+        fid.write("constant double Gauss%dZ[%d]={\n"%(n, array_size))
+        fid.write(",\n".join("\t% .15e"%v for v in z))
+        fid.write("\n};")
Index: sasmodels/guyou.py
===================================================================
--- sasmodels/guyou.py	(revision 0d5a655d6ddebe6096fad050347df4796b956767)
+++ sasmodels/guyou.py	(revision 0d5a655d6ddebe6096fad050347df4796b956767)
@@ -0,0 +1,375 @@
+# Copyright 2013-2016 Mike Bostock
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of the author nor the names of contributors may be used to
+#   endorse or promote products derived from this software without specific prior
+#   written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# https://github.com/d3/d3-geo-projection
+# commit fd2886555e46b35163c7b898d43c7d1bcebbba7c 2016-07-02
+#
+# 2017-11-01 Paul Kienzle
+# * converted to python, using degrees rather than radians
+from __future__ import division, print_function
+
+import numpy as np
+from numpy import sqrt, pi, tan, cos, sin, log, exp, arctan2 as atan2, sign, radians, degrees
+
+_ = """
+# mpmath version of special functions
+import mpmath as mp
+sn, cn, dn = [mp.ellipfun(v) for v in 'sn', 'cn', 'dn']
+
+def ellipticJi(u, v, m):
+    z = u+v*1j
+    return sn(z, m), cn(z, m), dn(z, m)
+
+def ellipticJ(u, m):
+    s, c, d = sn(u, m), cn(u, m), dn(u, m)
+    phi = mp.asin(s)
+    return s, c, d, phi
+
+def ellipticFi(phi, psi, m):
+    z = phi + psi*1j
+    return mp.ellipf(z, m)
+
+def ellipticF(phi, m):
+    return mp.ellipf(phi, m)
+"""
+
+# scipy version of special functions
+from scipy.special import ellipj as ellipticJ, ellipkinc as ellipticF
+from numpy import sinh, sign, arctan as atan
+
+def ellipticJi(u, v, m):
+    scalar = np.isscalar(u) and np.isscalar(v) and np.isscalar(m)
+    u, v, m = np.broadcast_arrays(u, v, m)
+    result = np.empty_like([u,u,u], 'D')
+    real = v==0
+    imag = u==0
+    mixed = ~(real|imag)
+    result[:, real] = _ellipticJi_real(u[real], m[real])
+    result[:, imag] = _ellipticJi_imag(v[imag], m[imag])
+    result[:, mixed] = _ellipticJi(u[mixed], v[mixed], m[mixed])
+    return result[0,:] if scalar else result
+
+def _ellipticJi_real(u, m):
+    sn, cn, dn, phi = ellipticJ(u, m)
+    return sn, cn, dn
+
+def _ellipticJi_imag(v, m):
+    sn, cn, dn, phi = ellipticJ(v, 1-m)
+    return 1j*sn/cn, 1/cn, dn/cn
+
+def _ellipticJi(u, v, m):
+    # Ignoring special cases for now.
+    #   u=0: (1j*b[0]/b[1], 1/b[1], b[2]/b[1])
+    #   v=0: (a[0], a[1], a[2])
+    a = ellipticJ(u, m)
+    b = ellipticJ(v, 1 - m)
+    c = b[1]**2 + m * (a[0] * b[0])**2
+    return [
+        (a[0] * b[2] / c) + 1j*(a[1] * a[2] * b[0] * b[1] / c),
+        (a[1] * b[1] / c) + 1j*(-a[0] * a[2] * b[0] * b[2] / c),
+        (a[2] * b[1] * b[2] / c) + 1j*(-m * a[0] * a[1] * b[0] / c),
+    ]
+
+# calculate F(phi+ipsi|m).
+# see Abramowitz and Stegun, 17.4.11.
+def ellipticFi(phi, psi, m):
+    if np.any(phi == 0):
+        scalar = np.isscalar(phi) and np.isscalar(psi) and np.isscalar(m)
+        phi, psi, m = np.broadcast_arrays(phi, psi, m)
+        result = np.empty_like(phi, 'D')
+        index = (phi == 0)
+        result[index] = ellipticF(atan(sinh(abs(phi[index]))), 1-m[index]) * sign(psi[index])
+        result[~index] = ellipticFi(phi[~index], psi[~index], m[~index])
+        return result.reshape(1)[0] if scalar else result
+
+    r = abs(phi)
+    i = abs(psi)
+    sinhpsi2 = sinh(i)**2
+    cscphi2 = 1 / sin(r)**2
+    cotphi2 = 1 / tan(r)**2
+    b = -(cotphi2 + m * (sinhpsi2 * cscphi2) - 1 + m)
+    c = (m - 1) * cotphi2
+    cotlambda2 = (-b + sqrt(b * b - 4 * c)) / 2
+    re = ellipticF(atan(1 / sqrt(cotlambda2)), m) * sign(phi)
+    im = ellipticF(atan(sqrt(np.maximum(0,(cotlambda2 / cotphi2 - 1) / m))), 1 - m) * sign(psi)
+    return re + 1j*im
+
+sqrt2 = sqrt(2)
+
+# [PAK] renamed k_ => cos_u, k => sin_u, k*k => sinsq_u to avoid k,K confusion
+# cos_u = 0.171572875253809902396622551580603842860656249246103853646...
+# sinsq_u = 0.970562748477140585620264690516376942836062504523376878120...
+# K = 3.165103454447431823666270142140819753058976299237578486994...
+def guyou(lam, phi):
+    # [PAK] wrap into [-pi/2, pi/2] radians
+    x, y = np.asarray(lam), np.asarray(phi)
+    xn, x = divmod(x+90, 180)
+    yn, y = divmod(y+90, 180)
+    xn, lam = xn*180, radians(x-90)
+    yn, phi = yn*180, radians(y-90)
+
+    # Compute constant K
+    cos_u = (sqrt2 - 1) / (sqrt2 + 1)
+    sinsq_u = 1 - cos_u**2
+    K = ellipticF(pi/2, sinsq_u)
+
+    # [PAK] simplify expressions, using the fact that f = -1
+    # Note: exp(f log(x)) => 1/x,  cos(f x) => cos(x), sin(f x) => -sin(x)
+    r = 1/(tan(pi/4 + abs(phi)/2) * sqrt(cos_u))
+    at = atan(r * (cos(lam) - 1j*sin(lam)))
+    t = ellipticFi(at.real, at.imag, sinsq_u)
+    x, y = (-t.imag, sign(phi + (phi==0))*(0.5 * K - t.real))
+
+    # [PAK] convert to degrees, and return to original tile
+    return degrees(x)+xn, degrees(y)+yn
+
+def guyou_invert(x, y):
+    # [PAK] wrap into [-pi/2, pi/2] radians
+    x, y = np.asarray(x), np.asarray(y)
+    xn, x = divmod(x+90, 180)
+    yn, y = divmod(y+90, 180)
+    xn, x = xn*180, radians(x-90)
+    yn, y = yn*180, radians(y-90)
+
+    # compute constant K
+    cos_u = (sqrt2 - 1) / (sqrt2 + 1)
+    sinsq_u = 1 - cos_u**2
+    K = ellipticF(pi/2, sinsq_u)
+
+    # [PAK] simplify expressions, using the fact that f = -1
+    j = ellipticJi(K/2 - y, -x, sinsq_u)
+    tn = j[0]/j[1]  # j[0], j[1] are complex
+    # Note: -atan2(im(x)/re(x)) => angle(x)
+    lam = -np.angle(tn)
+    # Note: exp(0.5/f log(a re(x)^2 + a im(x)^2)) => 1/(sqrt(a) |x|)
+    phi = 2*atan(1/sqrt(cos_u)/abs(tn)) - pi/2
+
+    # [PAK] convert to degrees, and return to original tile
+    return degrees(lam)+xn, degrees(phi)+yn
+
+def plot_grid():
+    import matplotlib.pyplot as plt
+    from numpy import linspace
+    lat_line = linspace(-90, 90, 400)
+    long_line = linspace(-90, 90, 400)
+
+    #scale = 1
+    limit, step, scale = 90, 10, 2
+    plt.subplot(211)
+    for lat in range(-limit, limit+1, step):
+        x, y = guyou(scale*lat_line, scale*lat)
+        plt.plot(x, y, 'g')
+
+    for long in range(-limit, limit+1, step):
+        x, y = guyou(scale*long, scale*long_line)
+        plt.plot(x, y, 'b')
+    #plt.xlabel('longitude')
+    plt.ylabel('latitude')
+
+    plt.subplot(212)
+    for lat in range(-limit, limit+1, step):
+        x, y = guyou_invert(scale*lat_line, scale*lat)
+        plt.plot(x, y, 'g')
+
+    for long in range(-limit, limit+1, step):
+        x, y = guyou_invert(scale*long, scale*long_line)
+        plt.plot(x, y, 'b')
+    plt.xlabel('longitude')
+    plt.ylabel('latitude')
+
+if __name__ == "__main__":
+    plot_grid()
+    import matplotlib.pyplot as plt; plt.show()
+
+
+_ = """
+// Javascript source for elliptic functions
+//
+// Returns [sn, cn, dn](u + iv|m).
+export function ellipticJi(u, v, m) {
+  var a, b, c;
+  if (!u) {
+    b = ellipticJ(v, 1 - m);
+    return [
+      [0, b[0] / b[1]],
+      [1 / b[1], 0],
+      [b[2] / b[1], 0]
+    ];
+  }
+  a = ellipticJ(u, m);
+  if (!v) return [[a[0], 0], [a[1], 0], [a[2], 0]];
+  b = ellipticJ(v, 1 - m);
+  c = b[1] * b[1] + m * a[0] * a[0] * b[0] * b[0];
+  return [
+    [a[0] * b[2] / c, a[1] * a[2] * b[0] * b[1] / c],
+    [a[1] * b[1] / c, -a[0] * a[2] * b[0] * b[2] / c],
+    [a[2] * b[1] * b[2] / c, -m * a[0] * a[1] * b[0] / c]
+  ];
+}
+
+// Returns [sn, cn, dn, ph](u|m).
+export function ellipticJ(u, m) {
+  var ai, b, phi, t, twon;
+  if (m < epsilon) {
+    t = sin(u);
+    b = cos(u);
+    ai = m * (u - t * b) / 4;
+    return [
+      t - ai * b,
+      b + ai * t,
+      1 - m * t * t / 2,
+      u - ai
+    ];
+  }
+  if (m >= 1 - epsilon) {
+    ai = (1 - m) / 4;
+    b = cosh(u);
+    t = tanh(u);
+    phi = 1 / b;
+    twon = b * sinh(u);
+    return [
+      t + ai * (twon - u) / (b * b),
+      phi - ai * t * phi * (twon - u),
+      phi + ai * t * phi * (twon + u),
+      2 * atan(exp(u)) - halfPi + ai * (twon - u) / b
+    ];
+  }
+
+  var a = [1, 0, 0, 0, 0, 0, 0, 0, 0],
+      c = [sqrt(m), 0, 0, 0, 0, 0, 0, 0, 0],
+      i = 0;
+  b = sqrt(1 - m);
+  twon = 1;
+
+  while (abs(c[i] / a[i]) > epsilon && i < 8) {
+    ai = a[i++];
+    c[i] = (ai - b) / 2;
+    a[i] = (ai + b) / 2;
+    b = sqrt(ai * b);
+    twon *= 2;
+  }
+
+  phi = twon * a[i] * u;
+  do {
+    t = c[i] * sin(b = phi) / a[i];
+    phi = (asin(t) + phi) / 2;
+  } while (--i);
+
+  // [PAK] Cephes uses dn = sqrt(1 - m*sin^2 phi) rather than cos(phi)/cos(phi-b)
+  // DLMF says the second version is unstable near x = K.
+  return [sin(phi), t = cos(phi), t / cos(phi - b), phi];
+}
+
+// calculate F(phi+ipsi|m).
+// see Abramowitz and Stegun, 17.4.11.
+export function ellipticFi(phi, psi, m) {
+  var r = abs(phi),
+      i = abs(psi),
+      sinhpsi = sinh(i);
+  if (r) {
+    var cscphi = 1 / sin(r),
+        cotphi2 = 1 / (tan(r) * tan(r)),
+        b = -(cotphi2 + m * (sinhpsi * sinhpsi * cscphi * cscphi) - 1 + m),
+        c = (m - 1) * cotphi2,
+        cotlambda2 = (-b + sqrt(b * b - 4 * c)) / 2;
+    return [
+      ellipticF(atan(1 / sqrt(cotlambda2)), m) * sign(phi),
+      ellipticF(atan(sqrt((cotlambda2 / cotphi2 - 1) / m)), 1 - m) * sign(psi)
+    ];
+  }
+  return [
+    0,
+    ellipticF(atan(sinhpsi), 1 - m) * sign(psi)
+  ];
+}
+
+// Calculate F(phi|m) where m = k^2 = sin(alpha)^2.
+// See Abramowitz and Stegun, 17.6.7.
+export function ellipticF(phi, m) {
+  if (!m) return phi;
+  if (m === 1) return log(tan(phi / 2 + quarterPi));
+  var a = 1,
+      b = sqrt(1 - m),
+      c = sqrt(m);
+  for (var i = 0; abs(c) > epsilon; i++) {
+    if (phi % pi) {
+      var dPhi = atan(b * tan(phi) / a);
+      if (dPhi < 0) dPhi += pi;
+      phi += dPhi + ~~(phi / pi) * pi;
+    } else phi += phi;
+    c = (a + b) / 2;
+    b = sqrt(a * b);
+    c = ((a = c) - b) / 2;
+  }
+  return phi / (pow(2, i) * a);
+
+
+export function guyouRaw(lambda, phi) {
+  var k_ = (sqrt2 - 1) / (sqrt2 + 1),
+      k = sqrt(1 - k_ * k_),
+      K = ellipticF(halfPi, k * k),
+      f = -1,
+      psi = log(tan(pi / 4 + abs(phi) / 2)),
+      r = exp(f * psi) / sqrt(k_),
+      at = guyouComplexAtan(r * cos(f * lambda), r * sin(f * lambda)),
+      t = ellipticFi(at[0], at[1], k * k);
+  return [-t[1], (phi >= 0 ? 1 : -1) * (0.5 * K - t[0])];
+}
+
+function guyouComplexAtan(x, y) {
+  var x2 = x * x,
+      y_1 = y + 1,
+      t = 1 - x2 - y * y;
+  return [
+   0.5 * ((x >= 0 ? halfPi : -halfPi) - atan2(t, 2 * x)),
+    -0.25 * log(t * t + 4 * x2) +0.5 * log(y_1 * y_1 + x2)
+  ];
+}
+
+function guyouComplexDivide(a, b) {
+  var denominator = b[0] * b[0] + b[1] * b[1];
+  return [
+    (a[0] * b[0] + a[1] * b[1]) / denominator,
+    (a[1] * b[0] - a[0] * b[1]) / denominator
+  ];
+}
+
+guyouRaw.invert = function(x, y) {
+  var k_ = (sqrt2 - 1) / (sqrt2 + 1),
+      k = sqrt(1 - k_ * k_),
+      K = ellipticF(halfPi, k * k),
+      f = -1,
+      j = ellipticJi(0.5 * K - y, -x, k * k),
+      tn = guyouComplexDivide(j[0], j[1]),
+      lambda = atan2(tn[1], tn[0]) / f;
+  return [
+    lambda,
+    2 * atan(exp(0.5 / f * log(k_ * tn[0] * tn[0] + k_ * tn[1] * tn[1]))) - halfPi
+  ];
+};
+"""
Index: sasmodels/jitter.py
===================================================================
--- sasmodels/jitter.py	(revision 0d5a655d6ddebe6096fad050347df4796b956767)
+++ sasmodels/jitter.py	(revision 0d5a655d6ddebe6096fad050347df4796b956767)
@@ -0,0 +1,738 @@
+#!/usr/bin/env python
+"""
+Jitter Explorer
+===============
+
+Application to explore orientation angle and angular dispersity.
+"""
+from __future__ import division, print_function
+
+import sys, os
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
+
+import argparse
+
+import mpl_toolkits.mplot3d   # Adds projection='3d' option to subplot
+import matplotlib.pyplot as plt
+from matplotlib.widgets import Slider, CheckButtons
+from matplotlib import cm
+import numpy as np
+from numpy import pi, cos, sin, sqrt, exp, degrees, radians
+
+def draw_beam(ax, view=(0, 0)):
+    """
+    Draw the beam going from source at (0, 0, 1) to detector at (0, 0, -1)
+    """
+    #ax.plot([0,0],[0,0],[1,-1])
+    #ax.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]
+
+    ax.plot_surface(x, y, z, rstride=4, cstride=4, color='y', alpha=0.5)
+
+def draw_jitter(ax, view, jitter, dist='gaussian', size=(0.1, 0.4, 1.0)):
+    """
+    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)
+    draw_shape = draw_parallelepiped
+    #draw_shape = draw_ellipsoid
+
+    #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(ax, 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(ax, size, view, delta, alpha=0.8)
+    for v in 'xyz':
+        a, b, c = size
+        lim = np.sqrt(a**2+b**2+c**2)
+        getattr(ax, 'set_'+v+'lim')([-lim, lim])
+        getattr(ax, v+'axis').label.set_text(v)
+
+def draw_ellipsoid(ax, 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)
+
+    ax.plot_surface(x, y, z, rstride=4, cstride=4, color='w', alpha=alpha)
+
+    draw_labels(ax, 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]),
+    ])
+
+def draw_parallelepiped(ax, 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)
+    ax.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 "a+" 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, abs(x)*1.05, y, z)
+        ax.plot_trisurf(x, y, triangles=tri, Z=z, color=[1,0.6,0.6], alpha=alpha)
+
+    draw_labels(ax, 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]),
+    ])
+
+def draw_sphere(ax, 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))
+    ax.plot_surface(x, y, z, rstride=4, cstride=4, color='w')
+
+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',
+]
+def draw_mesh(ax, 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.
+    """
+    theta, phi, psi = view
+    dtheta, dphi, dpsi = jitter
+
+    t = np.linspace(-1, 1, n)
+    weights = np.ones_like(t)
+    if dist == 'gaussian':
+        t *= 3
+        weights = exp(-0.5*t**2)
+    elif dist == 'rectangle':
+        # Note: uses sasmodels ridiculous definition of rectangle width
+        t *= 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, wi, wj):
+            return wi*wj*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, wi, wj):
+            latitude = theta_i
+            scale = cos(radians(latitude))
+            w = 1 if abs(phi_j) < abs(scale)*180 else 0
+            return w*wi*wj
+    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, wi, wj):
+            return wi*wj
+    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, wi, wj):
+            # 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)
+            w = sin(radians(latitude))/latitude if latitude != 0 else 1
+            return w*wi*wj if latitude < 180 else 0
+    elif projection == 'azimuthal_equal_area':
+        def rotate(theta_i, phi_j):
+            R = min(1, sqrt(theta_i**2 + phi_j**2)/180)
+            latitude = 180-degrees(2*np.arccos(R))
+            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, wi, wj):
+            latitude = sqrt(theta_i**2 + phi_j**2)
+            w = sin(radians(latitude))/latitude if latitude != 0 else 1
+            return w*wi*wj if latitude < 180 else 0
+    else:
+        raise ValueError("unknown projection %r"%projection)
+
+    # mesh in theta, phi formed by rotating z
+    z = np.matrix([[0], [0], [radius]])
+    points = np.hstack([rotate(theta_i, phi_j)*z
+                        for theta_i in dtheta*t
+                        for phi_j in dphi*t])
+    # select just the active points (i.e., those with phi < 180
+    w = np.array([weight(theta_i, phi_j, wi, wj)
+                  for wi, theta_i in zip(weights, dtheta*t)
+                  for wj, phi_j in zip(weights, dphi*t)])
+    #print(max(w), min(w), min(w[w>0]))
+    points = points[:, w>0]
+    w = w[w>0]
+    w /= max(w)
+
+    if 0: # Kent distribution
+        points = np.hstack([Rx(phi_j)*Ry(theta_i)*z for theta_i in 30*t for phi_j in 60*t])
+        xp, yp, zp = [np.array(v).flatten() for v in points]
+        kappa = max(1e6, radians(dtheta)/(2*pi))
+        beta = 1/max(1e-6, radians(dphi)/(2*pi))/kappa
+        w = exp(kappa*zp) #+ beta*(xp**2 + yp**2)
+        print(kappa, dtheta, radians(dtheta), min(w), max(w), sum(w))
+        #w /= abs(cos(radians(
+        #w /= sum(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))
+    ax.scatter(x, y, z, c=w, marker='o', vmin=0., vmax=1.)
+
+def draw_labels(ax, 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()
+        ax.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
+def Rx(angle):
+    """Construct a matrix to rotate points about *x* by *angle* degrees."""
+    a = radians(angle)
+    R = [[1, 0, 0],
+         [0, +cos(a), -sin(a)],
+         [0, +sin(a), +cos(a)]]
+    return np.matrix(R)
+
+def Ry(angle):
+    """Construct a matrix to rotate points about *y* by *angle* degrees."""
+    a = radians(angle)
+    R = [[+cos(a), 0, +sin(a)],
+         [0, 1, 0],
+         [-sin(a), 0, +cos(a)]]
+    return np.matrix(R)
+
+def Rz(angle):
+    """Construct a matrix to rotate points about *z* by *angle* degrees."""
+    a = radians(angle)
+    R = [[+cos(a), -sin(a), 0],
+         [+sin(a), +cos(a), 0],
+         [0, 0, 1]]
+    return np.matrix(R)
+
+def transform_xyz(view, jitter, x, y, z):
+    """
+    Send a set of (x,y,z) points through the jitter and view transforms.
+    """
+    x, y, z = [np.asarray(v) for v in (x, y, z)]
+    shape = x.shape
+    points = np.matrix([x.flatten(),y.flatten(),z.flatten()])
+    points = apply_jitter(jitter, points)
+    points = orient_relative_to_beam(view, points)
+    x, y, z = [np.array(v).reshape(shape) for v in points]
+    return x, y, z
+
+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
+
+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
+}
+
+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]
+
+def draw_scattering(calculator, ax, view, jitter, dist='gaussian'):
+    """
+    Plot the scattering for the particular view.
+
+    *calculator* is returned from :func:`build_model`.  *ax* 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)
+        ax.plot_surface(qx, qy, -1.1, rstride=1, cstride=1, facecolors=colors)
+    elif 1:
+        ax.contourf(qx/qx.max(), qy/qy.max(), Iqxy, zdir='z', offset=-1.1,
+                    levels=np.linspace(vmin, vmax, 24))
+    else:
+        ax.pcolormesh(qx, qy, Iqxy)
+
+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
+    from sasmodels.data import empty_data2D
+    from sasmodels.direct_model import DirectModel
+
+    model_info = load_model_info(model_name)
+    model = build_model(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
+
+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
+    if model_name == 'sphere':
+        calculator = build_model('sphere', n=n, radius=c)
+        a = b = c
+    elif model_name == 'bcc_paracrystal':
+        calculator = build_model('bcc_paracrystal', n=n, dnn=c,
+                                  d_factor=0.06, radius=40)
+        a = b = c
+    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', 'triaxial_ellipsoid', 'bcc_paracrystal',
+    'cylinder', 'ellipsoid',
+    'sphere',
+ ]
+
+DISTRIBUTIONS = [
+    'gaussian', 'rectangle', 'uniform',
+]
+DIST_LIMITS = {
+    'gaussian': 30,
+    'rectangle': 90/sqrt(3),
+    'uniform': 90,
+}
+
+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, cylinder, ellipsoid, parallelepiped,
+    triaxial_ellipsoid, or 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)
+
+    ## 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])
+    #ax = plt.subplot(gs[0], projection='3d')
+    ax = plt.axes([0.0, 0.2, 1.0, 0.8], projection='3d')
+    try:  # CRUFT: not all versions of matplotlib accept 'square' 3d projection
+        ax.axis('square')
+    except Exception:
+        pass
+
+    axcolor = 'lightgoldenrodyellow'
+
+    ## add control widgets to plot
+    axtheta  = plt.axes([0.1, 0.15, 0.45, 0.04], axisbg=axcolor)
+    axphi = plt.axes([0.1, 0.1, 0.45, 0.04], axisbg=axcolor)
+    axpsi = plt.axes([0.1, 0.05, 0.45, 0.04], axisbg=axcolor)
+    stheta = Slider(axtheta, 'Theta', -90, 90, valinit=theta)
+    sphi = Slider(axphi, 'Phi', -180, 180, valinit=phi)
+    spsi = Slider(axpsi, 'Psi', -180, 180, valinit=psi)
+
+    axdtheta  = plt.axes([0.75, 0.15, 0.15, 0.04], axisbg=axcolor)
+    axdphi = plt.axes([0.75, 0.1, 0.15, 0.04], axisbg=axcolor)
+    axdpsi= 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(axdtheta, 'dTheta', 0, 2*dlimit, valinit=dtheta)
+    sdphi = Slider(axdphi, 'dPhi', 0, 2*dlimit, valinit=dphi)
+    sdpsi = Slider(axdpsi, '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, 0.5, 5][dims]
+        jitter = [0 if v < limit else v for v in jitter]
+        ax.cla()
+        draw_beam(ax, (0, 0))
+        draw_jitter(ax, view, jitter, dist=dist, size=size)
+        #draw_jitter(ax, view, (0,0,0))
+        draw_mesh(ax, view, jitter, dist=dist, n=mesh, projection=projection)
+        draw_scattering(calculator, ax, 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()
+
+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()
Index: sasmodels/kernel_header.c
===================================================================
--- sasmodels/kernel_header.c	(revision 8698a0ddea5a155fcc2e3f1085f06a618b46eebf)
+++ sasmodels/kernel_header.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -150,2 +150,69 @@
 inline double cube(double x) { return x*x*x; }
 inline double sas_sinx_x(double x) { return x==0 ? 1.0 : sin(x)/x; }
+
+// CRUFT: support old style models with orientation received qx, qy and angles
+
+// To rotate from the canonical position to theta, phi, psi, first rotate by
+// psi about the major axis, oriented along z, which is a rotation in the
+// detector plane xy. Next rotate by theta about the y axis, aligning the major
+// axis in the xz plane. Finally, rotate by phi in the detector plane xy.
+// To compute the scattering, undo these rotations in reverse order:
+//     rotate in xy by -phi, rotate in xz by -theta, rotate in xy by -psi
+// The returned q is the length of the q vector and (xhat, yhat, zhat) is a unit
+// vector in the q direction.
+// To change between counterclockwise and clockwise rotation, change the
+// sign of phi and psi.
+
+#if 1
+//think cos(theta) should be sin(theta) in new coords, RKH 11Jan2017
+#define ORIENT_SYMMETRIC(qx, qy, theta, phi, q, sn, cn) do { \
+    SINCOS(phi*M_PI_180, sn, cn); \
+    q = sqrt(qx*qx + qy*qy); \
+    cn  = (q==0. ? 1.0 : (cn*qx + sn*qy)/q * sin(theta*M_PI_180));  \
+    sn = sqrt(1 - cn*cn); \
+    } while (0)
+#else
+// SasView 3.x definition of orientation
+#define ORIENT_SYMMETRIC(qx, qy, theta, phi, q, sn, cn) do { \
+    SINCOS(theta*M_PI_180, sn, cn); \
+    q = sqrt(qx*qx + qy*qy);\
+    cn = (q==0. ? 1.0 : (cn*cos(phi*M_PI_180)*qx + sn*qy)/q); \
+    sn = sqrt(1 - cn*cn); \
+    } while (0)
+#endif
+
+#if 1
+#define ORIENT_ASYMMETRIC(qx, qy, theta, phi, psi, q, xhat, yhat, zhat) do { \
+    q = sqrt(qx*qx + qy*qy); \
+    const double qxhat = qx/q; \
+    const double qyhat = qy/q; \
+    double sin_theta, cos_theta; \
+    double sin_phi, cos_phi; \
+    double sin_psi, cos_psi; \
+    SINCOS(theta*M_PI_180, sin_theta, cos_theta); \
+    SINCOS(phi*M_PI_180, sin_phi, cos_phi); \
+    SINCOS(psi*M_PI_180, sin_psi, cos_psi); \
+    xhat = qxhat*(-sin_phi*sin_psi + cos_theta*cos_phi*cos_psi) \
+         + qyhat*( cos_phi*sin_psi + cos_theta*sin_phi*cos_psi); \
+    yhat = qxhat*(-sin_phi*cos_psi - cos_theta*cos_phi*sin_psi) \
+         + qyhat*( cos_phi*cos_psi - cos_theta*sin_phi*sin_psi); \
+    zhat = qxhat*(-sin_theta*cos_phi) \
+         + qyhat*(-sin_theta*sin_phi); \
+    } while (0)
+#else
+// SasView 3.x definition of orientation
+#define ORIENT_ASYMMETRIC(qx, qy, theta, phi, psi, q, cos_alpha, cos_mu, cos_nu) do { \
+    q = sqrt(qx*qx + qy*qy); \
+    const double qxhat = qx/q; \
+    const double qyhat = qy/q; \
+    double sin_theta, cos_theta; \
+    double sin_phi, cos_phi; \
+    double sin_psi, cos_psi; \
+    SINCOS(theta*M_PI_180, sin_theta, cos_theta); \
+    SINCOS(phi*M_PI_180, sin_phi, cos_phi); \
+    SINCOS(psi*M_PI_180, sin_psi, cos_psi); \
+    cos_alpha = cos_theta*cos_phi*qxhat + sin_theta*qyhat; \
+    cos_mu = (-sin_theta*cos_psi*cos_phi - sin_psi*sin_phi)*qxhat + cos_theta*cos_psi*qyhat; \
+    cos_nu = (-cos_phi*sin_psi*sin_theta + sin_phi*cos_psi)*qxhat + sin_psi*cos_theta*qyhat; \
+    } while (0)
+#endif
Index: sasmodels/kernel_iq.c
===================================================================
--- sasmodels/kernel_iq.c	(revision 6aee3abf255884200f80256eb642d49c1e9c888e)
+++ sasmodels/kernel_iq.c	(revision ec8d4acf126bf6a5404bc968d4eea25959657b12)
@@ -31,4 +31,5 @@
 //  CALL_IQ_AC(qa, qc, table) : call the Iqxy function for symmetric shapes
 //  CALL_IQ_ABC(qa, qc, table) : call the Iqxy function for asymmetric shapes
+//  CALL_IQ_XY(qx, qy, table) : call the Iqxy function for arbitrary models
 //  INVALID(table) : test if the current point is feesible to calculate.  This
 //      will be defined in the kernel definition file.
@@ -173,9 +174,9 @@
 static double
 qac_apply(
-    QACRotation rotation,
+    QACRotation *rotation,
     double qx, double qy,
     double *qa_out, double *qc_out)
 {
-    const double dqc = rotation.R31*qx + rotation.R32*qy;
+    const double dqc = rotation->R31*qx + rotation->R32*qy;
     // Indirect calculation of qab, from qab^2 = |q|^2 - qc^2
     const double dqa = sqrt(-dqc*dqc + qx*qx + qy*qy);
@@ -246,11 +247,11 @@
 static double
 qabc_apply(
-    QABCRotation rotation,
+    QABCRotation *rotation,
     double qx, double qy,
     double *qa_out, double *qb_out, double *qc_out)
 {
-    *qa_out = rotation.R11*qx + rotation.R12*qy;
-    *qb_out = rotation.R21*qx + rotation.R22*qy;
-    *qc_out = rotation.R31*qx + rotation.R32*qy;
+    *qa_out = rotation->R11*qx + rotation->R12*qy;
+    *qb_out = rotation->R21*qx + rotation->R22*qy;
+    *qc_out = rotation->R31*qx + rotation->R32*qy;
 }
 
@@ -453,5 +454,5 @@
   // theta, phi, dtheta, dphi are defined below in projection to avoid repeated code.
   #define BUILD_ROTATION() qac_rotation(&rotation, theta, phi, dtheta, dphi);
-  #define APPLY_ROTATION() qac_apply(rotation, qx, qy, &qa, &qc)
+  #define APPLY_ROTATION() qac_apply(&rotation, qx, qy, &qa, &qc)
   #define CALL_KERNEL() CALL_IQ_AC(qa, qc, local_values.table)
 
@@ -467,15 +468,52 @@
   local_values.table.psi = 0.;
   #define BUILD_ROTATION() qabc_rotation(&rotation, theta, phi, psi, dtheta, dphi, local_values.table.psi)
-  #define APPLY_ROTATION() qabc_apply(rotation, qx, qy, &qa, &qb, &qc)
+  #define APPLY_ROTATION() qabc_apply(&rotation, qx, qy, &qa, &qb, &qc)
   #define CALL_KERNEL() CALL_IQ_ABC(qa, qb, qc, local_values.table)
-#endif
-
-// Doing jitter projection code outside the previous if block so that we don't
-// need to repeat the identical logic in the IQ_AC and IQ_ABC branches.  This
-// will become more important if we implement more projections, or more
-// complicated projections.
-#if defined(CALL_IQ) || defined(CALL_IQ_A)
+#elif defined(CALL_IQ_XY)
+  // direct call to qx,qy calculator
+  double qx, qy;
+  #define FETCH_Q() do { qx = q[2*q_index]; qy = q[2*q_index+1]; } while (0)
+  #define BUILD_ROTATION() do {} while(0)
+  #define APPLY_ROTATION() do {} while(0)
+  #define CALL_KERNEL() CALL_IQ_XY(qx, qy, local_values.table)
+#endif
+
+// Define APPLY_PROJECTION depending on model symmetries. We do this outside
+// the previous if block so that we don't need to repeat the identical
+// logic in the IQ_AC and IQ_ABC branches.  This will become more important
+// if we implement more projections, or more complicated projections.
+#if defined(CALL_IQ) || defined(CALL_IQ_A)  // no orientation
   #define APPLY_PROJECTION() const double weight=weight0
-#else // !spherosymmetric projection
+#elif defined(CALL_IQ_XY) // pass orientation to the model
+  // CRUFT: support oriented model which define Iqxy rather than Iqac or Iqabc
+  // Need to plug the values for the orientation angles back into parameter
+  // table in case they were overridden by the orientation offset.  This
+  // means that orientation dispersity will not work for these models, but
+  // it was broken anyway, so no matter.  Still want to provide Iqxy in case
+  // the user model wants full control of orientation/magnetism.
+  #if defined(HAVE_PSI)
+    const double theta = values[details->theta_par+2];
+    const double phi = values[details->theta_par+3];
+    const double psi = values[details->theta_par+4];
+    double weight;
+    #define APPLY_PROJECTION() do { \
+      local_values.table.theta = theta; \
+      local_values.table.phi = phi; \
+      local_values.table.psi = psi; \
+      weight=weight0; \
+    } while (0)
+  #elif defined(HAVE_THETA)
+    const double theta = values[details->theta_par+2];
+    const double phi = values[details->theta_par+3];
+    double weight;
+    #define APPLY_PROJECTION() do { \
+      local_values.table.theta = theta; \
+      local_values.table.phi = phi; \
+      weight=weight0; \
+    } while (0)
+  #else
+    #define APPLY_PROJECTION() const double weight=weight0
+  #endif
+#else // apply jitter and view before calling the model
   // Grab the "view" angles (theta, phi, psi) from the initial parameter table.
   const double theta = values[details->theta_par+2];
@@ -488,5 +526,5 @@
   // we go through the mesh.
   double dtheta, dphi, weight;
-  #if PROJECTION == 1
+  #if PROJECTION == 1 // equirectangular
     #define APPLY_PROJECTION() do { \
       dtheta = local_values.table.theta; \
@@ -494,5 +532,5 @@
       weight = fabs(cos(dtheta*M_PI_180)) * weight0; \
     } while (0)
-  #elif PROJECTION == 2
+  #elif PROJECTION == 2 // sinusoidal
     #define APPLY_PROJECTION() do { \
       dtheta = local_values.table.theta; \
@@ -504,5 +542,5 @@
     } while (0)
   #endif
-#endif // !spherosymmetric projection
+#endif // done defining APPLY_PROJECTION
 
 // ** define looping macros **
Index: sasmodels/kernelpy.py
===================================================================
--- sasmodels/kernelpy.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/kernelpy.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -26,4 +26,6 @@
 # pylint: enable=unused-import
 
+logger = logging.getLogger(__name__)
+
 class PyModel(KernelModel):
     """
@@ -31,14 +33,13 @@
     """
     def __init__(self, model_info):
-        # Make sure Iq and Iqxy are available and vectorized
+        # Make sure Iq is available and vectorized
         _create_default_functions(model_info)
         self.info = model_info
         self.dtype = np.dtype('d')
-        logging.info("load python model " + self.info.name)
+        logger.info("load python model " + self.info.name)
 
     def make_kernel(self, q_vectors):
         q_input = PyInput(q_vectors, dtype=F64)
-        kernel = self.info.Iqxy if q_input.is_2d else self.info.Iq
-        return PyKernel(kernel, self.info, q_input)
+        return PyKernel(self.info, q_input)
 
     def release(self):
@@ -89,5 +90,5 @@
     Callable SAS kernel.
 
-    *kernel* is the DllKernel object to call.
+    *kernel* is the kernel object to call.
 
     *model_info* is the module information
@@ -104,5 +105,5 @@
     Call :meth:`release` when done with the kernel instance.
     """
-    def __init__(self, kernel, model_info, q_input):
+    def __init__(self, model_info, q_input):
         # type: (callable, ModelInfo, List[np.ndarray]) -> None
         self.dtype = np.dtype('d')
@@ -110,5 +111,4 @@
         self.q_input = q_input
         self.res = np.empty(q_input.nq, q_input.dtype)
-        self.kernel = kernel
         self.dim = '2d' if q_input.is_2d else '1d'
 
@@ -159,6 +159,6 @@
         # Generate a closure which calls the form_volume if it exists.
         form_volume = model_info.form_volume
-        self._volume = ((lambda: form_volume(*volume_args)) if form_volume
-                        else (lambda: 1.0))
+        self._volume = ((lambda: form_volume(*volume_args)) if form_volume else
+                        (lambda: 1.0))
 
     def __call__(self, call_details, values, cutoff, magnetic):
@@ -261,6 +261,7 @@
     any functions that are not already marked as vectorized.
     """
+    # Note: must call create_vector_Iq before create_vector_Iqxy
     _create_vector_Iq(model_info)
-    _create_vector_Iqxy(model_info)  # call create_vector_Iq() first
+    _create_vector_Iqxy(model_info)
 
 
@@ -280,9 +281,10 @@
         model_info.Iq = vector_Iq
 
+
 def _create_vector_Iqxy(model_info):
     """
     Define Iqxy as a vector function if it exists, or default it from Iq().
     """
-    Iq, Iqxy = model_info.Iq, model_info.Iqxy
+    Iqxy = getattr(model_info, 'Iqxy', None)
     if callable(Iqxy):
         if not getattr(Iqxy, 'vectorized', False):
@@ -295,7 +297,8 @@
             vector_Iqxy.vectorized = True
             model_info.Iqxy = vector_Iqxy
-    elif callable(Iq):
+    else:
         #print("defaulting Iqxy")
         # Iq is vectorized because create_vector_Iq was already called.
+        Iq = model_info.Iq
         def default_Iqxy(qx, qy, *args):
             """
Index: sasmodels/modelinfo.py
===================================================================
--- sasmodels/modelinfo.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/modelinfo.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -37,5 +37,5 @@
 
 # assumptions about common parameters exist throughout the code, such as:
-# (1) kernel functions Iq, Iqxy, form_volume, ... don't see them
+# (1) kernel functions Iq, Iqxy, Iqac, Iqabc, form_volume, ... don't see them
 # (2) kernel drivers assume scale is par[0] and background is par[1]
 # (3) mixture models drop the background on components and replace the scale
@@ -256,7 +256,7 @@
 
     *type* indicates how the parameter will be used.  "volume" parameters
-    will be used in all functions.  "orientation" parameters will be used
-    in *Iqxy* and *Imagnetic*.  "magnetic* parameters will be used in
-    *Imagnetic* only.  If *type* is the empty string, the parameter will
+    will be used in all functions.  "orientation" parameters are not passed,
+    but will be used to convert from *qx*, *qy* to *qa*, *qb*, *qc* in calls to
+    *Iqxy* and *Imagnetic*.  If *type* is the empty string, the parameter will
     be used in all of *Iq*, *Iqxy* and *Imagnetic*.  "sld" parameters
     can automatically be promoted to magnetic parameters, each of which
@@ -386,7 +386,4 @@
       with vector parameter p sent as p[].
 
-    * [removed] *iqxy_parameters* is the list of parameters to the Iqxy(qx, qy, ...)
-      function, with vector parameter p sent as p[].
-
     * *form_volume_parameters* is the list of parameters to the form_volume(...)
       function, with vector parameter p sent as p[].
@@ -443,7 +440,6 @@
         self.iq_parameters = [p for p in self.kernel_parameters
                               if p.type not in ('orientation', 'magnetic')]
-        # note: orientation no longer sent to Iqxy, so its the same as
-        #self.iqxy_parameters = [p for p in self.kernel_parameters
-        #                        if p.type != 'magnetic']
+        self.orientation_parameters = [p for p in self.kernel_parameters
+                                       if p.type == 'orientation']
         self.form_volume_parameters = [p for p in self.kernel_parameters
                                        if p.type == 'volume']
@@ -490,9 +486,15 @@
                 if p.type != 'orientation':
                     raise TypeError("psi must be an orientation parameter")
+            elif p.type == 'orientation':
+                raise TypeError("only theta, phi and psi can be orientation parameters")
         if theta >= 0 and phi >= 0:
+            last_par = len(self.kernel_parameters) - 1
             if phi != theta+1:
                 raise TypeError("phi must follow theta")
             if psi >= 0 and psi != phi+1:
                 raise TypeError("psi must follow phi")
+            #if (psi >= 0 and psi != last_par) or (psi < 0 and phi != last_par):
+            #    raise TypeError("orientation parameters must appear at the "
+            #                    "end of the parameter table")
         elif theta >= 0 or phi >= 0 or psi >= 0:
             raise TypeError("oriented shapes must have both theta and phi and maybe psi")
@@ -715,4 +717,7 @@
 
 
+#: Set of variables defined in the model that might contain C code
+C_SYMBOLS = ['Imagnetic', 'Iq', 'Iqxy', 'Iqac', 'Iqabc', 'form_volume', 'c_code']
+
 def _find_source_lines(model_info, kernel_module):
     # type: (ModelInfo, ModuleType) -> None
@@ -720,37 +725,32 @@
     Identify the location of the C source inside the model definition file.
 
-    This code runs through the source of the kernel module looking for
-    lines that start with 'Iq', 'Iqxy' or 'form_volume'.  Clearly there are
-    all sorts of reasons why this might not work (e.g., code commented out
-    in a triple-quoted line block, code built using string concatenation,
-    or code defined in the branch of an 'if' block), but it should work
-    properly in the 95% case, and getting the incorrect line number will
-    be harmless.
-    """
-    # Check if we need line numbers at all
-    if callable(model_info.Iq):
-        return None
-
-    if (model_info.Iq is None
-            and model_info.Iqxy is None
-            and model_info.Imagnetic is None
-            and model_info.form_volume is None):
+    This code runs through the source of the kernel module looking for lines
+    that contain C code (because it is a c function definition).  Clearly
+    there are all sorts of reasons why this might not work (e.g., code
+    commented out in a triple-quoted line block, code built using string
+    concatenation, code defined in the branch of an 'if' block, code imported
+    from another file), but it should work properly in the 95% case, and for
+    the remainder, getting the incorrect line number will merely be
+    inconvenient.
+    """
+    # Only need line numbers if we are creating a C module and the C symbols
+    # are defined.
+    if (callable(model_info.Iq)
+            or not any(hasattr(model_info, s) for s in C_SYMBOLS)):
         return
 
-    # find the defintion lines for the different code blocks
+    # load the module source if we can
     try:
         source = inspect.getsource(kernel_module)
     except IOError:
         return
-    for k, v in enumerate(source.split('\n')):
-        if v.startswith('Imagnetic'):
-            model_info._Imagnetic_line = k+1
-        elif v.startswith('Iqxy'):
-            model_info._Iqxy_line = k+1
-        elif v.startswith('Iq'):
-            model_info._Iq_line = k+1
-        elif v.startswith('form_volume'):
-            model_info._form_volume_line = k+1
-
+
+    # look for symbol at the start of the line
+    for lineno, line in enumerate(source.split('\n')):
+        for name in C_SYMBOLS:
+            if line.startswith(name):
+                # Add 1 since some compilers complain about "#line 0"
+                model_info.lineno[name] = lineno + 1
+                break
 
 def make_model_info(kernel_module):
@@ -761,5 +761,5 @@
     Fill in default values for parts of the module that are not provided.
 
-    Note: vectorized Iq and Iqxy functions will be created for python
+    Note: vectorized Iq and Iqac/Iqabc functions will be created for python
     models when the model is first called, not when the model is loaded.
     """
@@ -790,4 +790,5 @@
     info.profile_axes = getattr(kernel_module, 'profile_axes', ['x', 'y'])
     info.source = getattr(kernel_module, 'source', [])
+    info.c_code = getattr(kernel_module, 'c_code', None)
     # TODO: check the structure of the tests
     info.tests = getattr(kernel_module, 'tests', [])
@@ -797,4 +798,6 @@
     info.Iq = getattr(kernel_module, 'Iq', None) # type: ignore
     info.Iqxy = getattr(kernel_module, 'Iqxy', None) # type: ignore
+    info.Iqac = getattr(kernel_module, 'Iqac', None) # type: ignore
+    info.Iqabc = getattr(kernel_module, 'Iqabc', None) # type: ignore
     info.Imagnetic = getattr(kernel_module, 'Imagnetic', None) # type: ignore
     info.profile = getattr(kernel_module, 'profile', None) # type: ignore
@@ -811,4 +814,8 @@
     info.hidden = getattr(kernel_module, 'hidden', None) # type: ignore
 
+    if callable(info.Iq) and parameters.has_2d:
+        raise ValueError("oriented python models not supported")
+
+    info.lineno = {}
     _find_source_lines(info, kernel_module)
 
@@ -824,5 +831,5 @@
 
     The structure should be mostly static, other than the delayed definition
-    of *Iq* and *Iqxy* if they need to be defined.
+    of *Iq*, *Iqac* and *Iqabc* if they need to be defined.
     """
     #: Full path to the file defining the kernel, if any.
@@ -906,9 +913,9 @@
     structure_factor = None # type: bool
     #: List of C source files used to define the model.  The source files
-    #: should define the *Iq* function, and possibly *Iqxy*, though a default
-    #: *Iqxy = Iq(sqrt(qx**2+qy**2)* will be created if no *Iqxy* is provided.
-    #: Files containing the most basic functions must appear first in the list,
-    #: followed by the files that use those functions.  Form factors are
-    #: indicated by providing a :attr:`ER` function.
+    #: should define the *Iq* function, and possibly *Iqac* or *Iqabc* if the
+    #: model defines orientation parameters. Files containing the most basic
+    #: functions must appear first in the list, followed by the files that
+    #: use those functions.  Form factors are indicated by providing
+    #: an :attr:`ER` function.
     source = None           # type: List[str]
     #: The set of tests that must pass.  The format of the tests is described
@@ -935,4 +942,8 @@
     #: See :attr:`ER` for details on the parameters.
     VR = None               # type: Optional[Callable[[np.ndarray], Tuple[np.ndarray, np.ndarray]]]
+    #: Arbitrary C code containing supporting functions, etc., to be inserted
+    #: after everything in source.  This can include Iq and Iqxy functions with
+    #: the full function signature, including all parameters.
+    c_code = None
     #: Returns the form volume for python-based models.  Form volume is needed
     #: for volume normalization in the polydispersity integral.  If no
@@ -955,6 +966,8 @@
     #: include the decimal point. See :mod:`generate` for more details.
     Iq = None               # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
-    #: Returns *I(qx, qy, a, b, ...)*.  The interface follows :attr:`Iq`.
-    Iqxy = None             # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
+    #: Returns *I(qab, qc, a, b, ...)*.  The interface follows :attr:`Iq`.
+    Iqac = None             # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
+    #: Returns *I(qa, qb, qc, a, b, ...)*.  The interface follows :attr:`Iq`.
+    Iqabc = None            # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
     #: Returns *I(qx, qy, a, b, ...)*.  The interface follows :attr:`Iq`.
     Imagnetic = None        # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
@@ -972,12 +985,6 @@
     #: Returns a random parameter set for the model
     random = None           # type: Optional[Callable[[], Dict[str, float]]]
-
-    # line numbers within the python file for bits of C source, if defined
-    # NB: some compilers fail with a "#line 0" directive, so default to 1.
-    _Imagnetic_line = 1
-    _Iqxy_line = 1
-    _Iq_line = 1
-    _form_volume_line = 1
-
+    #: Line numbers for symbols defining C code
+    lineno = None           # type: Dict[str, int]
 
     def __init__(self):
Index: sasmodels/models/_spherepy.py
===================================================================
--- sasmodels/models/_spherepy.py	(revision ef07e950965796e2a3a8156fcade98ce5e403001)
+++ sasmodels/models/_spherepy.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -88,8 +88,4 @@
 Iq.vectorized = True  # Iq accepts an array of q values
 
-def Iqxy(qx, qy, sld, sld_solvent, radius):
-    return Iq(sqrt(qx ** 2 + qy ** 2), sld, sld_solvent, radius)
-Iqxy.vectorized = True  # Iqxy accepts arrays of qx, qy values
-
 def sesans(z, sld, sld_solvent, radius):
     """
Index: sasmodels/models/barbell.c
===================================================================
--- sasmodels/models/barbell.c	(revision becded3d803d05553cd978e361eeb35b6f6736bf)
+++ sasmodels/models/barbell.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -23,10 +23,10 @@
     const double qab_r = radius_bell*qab; // Q*R*sin(theta)
     double total = 0.0;
-    for (int i = 0; i < 76; i++){
-        const double t = Gauss76Z[i]*zm + zb;
+    for (int i = 0; i < GAUSS_N; i++){
+        const double t = GAUSS_Z[i]*zm + zb;
         const double radical = 1.0 - t*t;
         const double bj = sas_2J1x_x(qab_r*sqrt(radical));
         const double Fq = cos(m*t + b) * radical * bj;
-        total += Gauss76Wt[i] * Fq;
+        total += GAUSS_W[i] * Fq;
     }
     // translate dx in [-1,1] to dx in [lower,upper]
@@ -73,10 +73,10 @@
     const double zb = M_PI_4;
     double total = 0.0;
-    for (int i = 0; i < 76; i++){
-        const double alpha= Gauss76Z[i]*zm + zb;
+    for (int i = 0; i < GAUSS_N; i++){
+        const double alpha= GAUSS_Z[i]*zm + zb;
         double sin_alpha, cos_alpha; // slots to hold sincos function output
         SINCOS(alpha, sin_alpha, cos_alpha);
         const double Aq = _fq(q*sin_alpha, q*cos_alpha, h, radius_bell, radius, half_length);
-        total += Gauss76Wt[i] * Aq * Aq * sin_alpha;
+        total += GAUSS_W[i] * Aq * Aq * sin_alpha;
     }
     // translate dx in [-1,1] to dx in [lower,upper]
@@ -90,5 +90,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double sld, double solvent_sld,
     double radius_bell, double radius, double length)
Index: sasmodels/models/bcc_paracrystal.c
===================================================================
--- sasmodels/models/bcc_paracrystal.c	(revision ea60e084e3e40eeb3abfc5f728359135acaed020)
+++ sasmodels/models/bcc_paracrystal.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -81,13 +81,13 @@
 
     double outer_sum = 0.0;
-    for(int i=0; i<150; i++) {
+    for(int i=0; i<GAUSS_N; i++) {
         double inner_sum = 0.0;
-        const double theta = Gauss150Z[i]*theta_m + theta_b;
+        const double theta = GAUSS_Z[i]*theta_m + theta_b;
         double sin_theta, cos_theta;
         SINCOS(theta, sin_theta, cos_theta);
         const double qc = q*cos_theta;
         const double qab = q*sin_theta;
-        for(int j=0;j<150;j++) {
-            const double phi = Gauss150Z[j]*phi_m + phi_b;
+        for(int j=0;j<GAUSS_N;j++) {
+            const double phi = GAUSS_Z[j]*phi_m + phi_b;
             double sin_phi, cos_phi;
             SINCOS(phi, sin_phi, cos_phi);
@@ -95,8 +95,8 @@
             const double qb = qab*sin_phi;
             const double form = bcc_Zq(qa, qb, qc, dnn, d_factor);
-            inner_sum += Gauss150Wt[j] * form;
+            inner_sum += GAUSS_W[j] * form;
         }
         inner_sum *= phi_m;  // sum(f(x)dx) = sum(f(x)) dx
-        outer_sum += Gauss150Wt[i] * inner_sum * sin_theta;
+        outer_sum += GAUSS_W[i] * inner_sum * sin_theta;
     }
     outer_sum *= theta_m;
@@ -107,5 +107,5 @@
 
 
-static double Iqxy(double qa, double qb, double qc,
+static double Iqabc(double qa, double qb, double qc,
     double dnn, double d_factor, double radius,
     double sld, double solvent_sld)
Index: sasmodels/models/capped_cylinder.c
===================================================================
--- sasmodels/models/capped_cylinder.c	(revision becded3d803d05553cd978e361eeb35b6f6736bf)
+++ sasmodels/models/capped_cylinder.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -30,10 +30,10 @@
     const double qab_r = radius_cap*qab; // Q*R*sin(theta)
     double total = 0.0;
-    for (int i=0; i<76 ;i++) {
-        const double t = Gauss76Z[i]*zm + zb;
+    for (int i=0; i<GAUSS_N; i++) {
+        const double t = GAUSS_Z[i]*zm + zb;
         const double radical = 1.0 - t*t;
         const double bj = sas_2J1x_x(qab_r*sqrt(radical));
         const double Fq = cos(m*t + b) * radical * bj;
-        total += Gauss76Wt[i] * Fq;
+        total += GAUSS_W[i] * Fq;
     }
     // translate dx in [-1,1] to dx in [lower,upper]
@@ -95,6 +95,6 @@
     const double zb = M_PI_4;
     double total = 0.0;
-    for (int i=0; i<76 ;i++) {
-        const double theta = Gauss76Z[i]*zm + zb;
+    for (int i=0; i<GAUSS_N ;i++) {
+        const double theta = GAUSS_Z[i]*zm + zb;
         double sin_theta, cos_theta; // slots to hold sincos function output
         SINCOS(theta, sin_theta, cos_theta);
@@ -103,5 +103,5 @@
         const double Aq = _fq(qab, qc, h, radius_cap, radius, half_length);
         // scale by sin_theta for spherical coord integration
-        total += Gauss76Wt[i] * Aq * Aq * sin_theta;
+        total += GAUSS_W[i] * Aq * Aq * sin_theta;
     }
     // translate dx in [-1,1] to dx in [lower,upper]
@@ -115,5 +115,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double sld, double solvent_sld, double radius,
     double radius_cap, double length)
Index: sasmodels/models/core_shell_bicelle.c
===================================================================
--- sasmodels/models/core_shell_bicelle.c	(revision becded3d803d05553cd978e361eeb35b6f6736bf)
+++ sasmodels/models/core_shell_bicelle.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -52,11 +52,11 @@
 
     double total = 0.0;
-    for(int i=0;i<N_POINTS_76;i++) {
-        double theta = (Gauss76Z[i] + 1.0)*uplim;
+    for(int i=0;i<GAUSS_N;i++) {
+        double theta = (GAUSS_Z[i] + 1.0)*uplim;
         double sin_theta, cos_theta; // slots to hold sincos function output
         SINCOS(theta, sin_theta, cos_theta);
         double fq = bicelle_kernel(q*sin_theta, q*cos_theta, radius, thick_radius, thick_face,
                                    halflength, sld_core, sld_face, sld_rim, sld_solvent);
-        total += Gauss76Wt[i]*fq*fq*sin_theta;
+        total += GAUSS_W[i]*fq*fq*sin_theta;
     }
 
@@ -67,5 +67,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double radius,
     double thick_rim,
Index: sasmodels/models/core_shell_bicelle_elliptical.c
===================================================================
--- sasmodels/models/core_shell_bicelle_elliptical.c	(revision 82592da3721b7e2098cf6346cf224814189fadb3)
+++ sasmodels/models/core_shell_bicelle_elliptical.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -37,10 +37,10 @@
     //initialize integral
     double outer_sum = 0.0;
-    for(int i=0;i<76;i++) {
+    for(int i=0;i<GAUSS_N;i++) {
         //setup inner integral over the ellipsoidal cross-section
         //const double va = 0.0;
         //const double vb = 1.0;
-        //const double cos_theta = ( Gauss76Z[i]*(vb-va) + va + vb )/2.0;
-        const double cos_theta = ( Gauss76Z[i] + 1.0 )/2.0;
+        //const double cos_theta = ( GAUSS_Z[i]*(vb-va) + va + vb )/2.0;
+        const double cos_theta = ( GAUSS_Z[i] + 1.0 )/2.0;
         const double sin_theta = sqrt(1.0 - cos_theta*cos_theta);
         const double qab = q*sin_theta;
@@ -49,11 +49,11 @@
         const double si2 = sas_sinx_x((halfheight+thick_face)*qc);
         double inner_sum=0.0;
-        for(int j=0;j<76;j++) {
+        for(int j=0;j<GAUSS_N;j++) {
             //76 gauss points for the inner integral (WAS 20 points,so this may make unecessarily slow, but playing safe)
             // inner integral limits
             //const double vaj=0.0;
             //const double vbj=M_PI;
-            //const double phi = ( Gauss76Z[j]*(vbj-vaj) + vaj + vbj )/2.0;
-            const double phi = ( Gauss76Z[j] +1.0)*M_PI_2;
+            //const double phi = ( GAUSS_Z[j]*(vbj-vaj) + vaj + vbj )/2.0;
+            const double phi = ( GAUSS_Z[j] +1.0)*M_PI_2;
             const double rr = sqrt(r2A - r2B*cos(phi));
             const double be1 = sas_2J1x_x(rr*qab);
@@ -61,8 +61,8 @@
             const double fq = dr1*si1*be1 + dr2*si2*be2 + dr3*si2*be1;
 
-            inner_sum += Gauss76Wt[j] * fq * fq;
+            inner_sum += GAUSS_W[j] * fq * fq;
         }
         //now calculate outer integral
-        outer_sum += Gauss76Wt[i] * inner_sum;
+        outer_sum += GAUSS_W[i] * inner_sum;
     }
 
@@ -71,5 +71,5 @@
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
     double r_minor,
     double x_core,
Index: sasmodels/models/core_shell_bicelle_elliptical_belt_rough.c
===================================================================
--- sasmodels/models/core_shell_bicelle_elliptical_belt_rough.c	(revision 82592da3721b7e2098cf6346cf224814189fadb3)
+++ sasmodels/models/core_shell_bicelle_elliptical_belt_rough.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -7,5 +7,5 @@
         double length)
 {
-    return M_PI*(  (r_minor + thick_rim)*(r_minor*x_core + thick_rim)* length + 
+    return M_PI*(  (r_minor + thick_rim)*(r_minor*x_core + thick_rim)* length +
                  square(r_minor)*x_core*2.0*thick_face  );
 }
@@ -47,9 +47,9 @@
     //initialize integral
     double outer_sum = 0.0;
-    for(int i=0;i<76;i++) {
+    for(int i=0;i<GAUSS_N;i++) {
         //setup inner integral over the ellipsoidal cross-section
         // since we generate these lots of times, why not store them somewhere?
-        //const double cos_alpha = ( Gauss76Z[i]*(vb-va) + va + vb )/2.0;
-        const double cos_alpha = ( Gauss76Z[i] + 1.0 )/2.0;
+        //const double cos_alpha = ( GAUSS_Z[i]*(vb-va) + va + vb )/2.0;
+        const double cos_alpha = ( GAUSS_Z[i] + 1.0 )/2.0;
         const double sin_alpha = sqrt(1.0 - cos_alpha*cos_alpha);
         double inner_sum=0;
@@ -58,8 +58,8 @@
         si1 = sas_sinx_x(sinarg1);
         si2 = sas_sinx_x(sinarg2);
-        for(int j=0;j<76;j++) {
+        for(int j=0;j<GAUSS_N;j++) {
             //76 gauss points for the inner integral (WAS 20 points,so this may make unecessarily slow, but playing safe)
-            //const double beta = ( Gauss76Z[j]*(vbj-vaj) + vaj + vbj )/2.0;
-            const double beta = ( Gauss76Z[j] +1.0)*M_PI_2;
+            //const double beta = ( GAUSS_Z[j]*(vbj-vaj) + vaj + vbj )/2.0;
+            const double beta = ( GAUSS_Z[j] +1.0)*M_PI_2;
             const double rr = sqrt(r2A - r2B*cos(beta));
             double besarg1 = q*rr*sin_alpha;
@@ -67,10 +67,10 @@
             be1 = sas_2J1x_x(besarg1);
             be2 = sas_2J1x_x(besarg2);
-            inner_sum += Gauss76Wt[j] *square(dr1*si1*be1 +
+            inner_sum += GAUSS_W[j] *square(dr1*si1*be1 +
                                               dr2*si1*be2 +
                                               dr3*si2*be1);
         }
         //now calculate outer integral
-        outer_sum += Gauss76Wt[i] * inner_sum;
+        outer_sum += GAUSS_W[i] * inner_sum;
     }
 
@@ -79,5 +79,5 @@
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
           double r_minor,
           double x_core,
@@ -114,3 +114,2 @@
     return 1.0e-4 * Aq*exp(-0.5*(square(qa) + square(qb) + square(qc) )*square(sigma));
 }
-
Index: sasmodels/models/core_shell_bicelle_elliptical_belt_rough.py
===================================================================
--- sasmodels/models/core_shell_bicelle_elliptical_belt_rough.py	(revision 110f69cfa1143196495c76df1d5cff08c6ce884e)
+++ sasmodels/models/core_shell_bicelle_elliptical_belt_rough.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -149,8 +149,8 @@
     ["sld_rim",        "1e-6/Ang^2", 1, [-inf, inf], "sld",         "Cylinder rim scattering length density"],
     ["sld_solvent",    "1e-6/Ang^2", 6, [-inf, inf], "sld",         "Solvent scattering length density"],
+    ["sigma",       "Ang",        0,    [0, inf],    "",            "interfacial roughness"],
     ["theta",       "degrees",    90.0, [-360, 360], "orientation", "cylinder axis to beam angle"],
     ["phi",         "degrees",    0,    [-360, 360], "orientation", "rotation about beam"],
     ["psi",         "degrees",    0,    [-360, 360], "orientation", "rotation about cylinder axis"],
-    ["sigma",       "Ang",        0,    [0, inf],    "",            "interfacial roughness"]
     ]
 
Index: sasmodels/models/core_shell_cylinder.c
===================================================================
--- sasmodels/models/core_shell_cylinder.c	(revision becded3d803d05553cd978e361eeb35b6f6736bf)
+++ sasmodels/models/core_shell_cylinder.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -30,9 +30,9 @@
     const double shell_vd = form_volume(radius,thickness,length) * (shell_sld-solvent_sld);
     double total = 0.0;
-    for (int i=0; i<76 ;i++) {
+    for (int i=0; i<GAUSS_N ;i++) {
         // translate a point in [-1,1] to a point in [0, pi/2]
-        //const double theta = ( Gauss76Z[i]*(upper-lower) + upper + lower )/2.0;
+        //const double theta = ( GAUSS_Z[i]*(upper-lower) + upper + lower )/2.0;
         double sin_theta, cos_theta;
-        const double theta = Gauss76Z[i]*M_PI_4 + M_PI_4;
+        const double theta = GAUSS_Z[i]*M_PI_4 + M_PI_4;
         SINCOS(theta, sin_theta,  cos_theta);
         const double qab = q*sin_theta;
@@ -40,5 +40,5 @@
         const double fq = _cyl(core_vd, core_r*qab, core_h*qc)
             + _cyl(shell_vd, shell_r*qab, shell_h*qc);
-        total += Gauss76Wt[i] * fq * fq * sin_theta;
+        total += GAUSS_W[i] * fq * fq * sin_theta;
     }
     // translate dx in [-1,1] to dx in [lower,upper]
@@ -48,5 +48,5 @@
 
 
-double Iqxy(double qab, double qc,
+double Iqac(double qab, double qc,
     double core_sld,
     double shell_sld,
Index: sasmodels/models/core_shell_ellipsoid.c
===================================================================
--- sasmodels/models/core_shell_ellipsoid.c	(revision becded3d803d05553cd978e361eeb35b6f6736bf)
+++ sasmodels/models/core_shell_ellipsoid.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -59,6 +59,6 @@
     const double b = 0.5;
     double total = 0.0;     //initialize intergral
-    for(int i=0;i<76;i++) {
-        const double cos_theta = Gauss76Z[i]*m + b;
+    for(int i=0;i<GAUSS_N;i++) {
+        const double cos_theta = GAUSS_Z[i]*m + b;
         const double sin_theta = sqrt(1.0 - cos_theta*cos_theta);
         double fq = _cs_ellipsoid_kernel(q*sin_theta, q*cos_theta,
@@ -66,5 +66,5 @@
             equat_shell, polar_shell,
             sld_core_shell, sld_shell_solvent);
-        total += Gauss76Wt[i] * fq * fq;
+        total += GAUSS_W[i] * fq * fq;
     }
     total *= m;
@@ -75,5 +75,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double radius_equat_core,
     double x_core,
Index: sasmodels/models/core_shell_parallelepiped.c
===================================================================
--- sasmodels/models/core_shell_parallelepiped.c	(revision c69d6d6b7d1b4a8e1acc9c734f6ec3b26084cdd7)
+++ sasmodels/models/core_shell_parallelepiped.c	(revision e0772316ffb087990c2e1b341c744594e3087097)
@@ -1,11 +1,28 @@
+// Set OVERLAPPING to 1 in order to fill in the edges of the box, with
+// c endcaps and b overlapping a.  With the proper choice of parameters,
+// (setting rim slds to sld, core sld to solvent, rim thickness to thickness
+// and subtracting 2*thickness from length, this should match the hollow
+// rectangular prism.)  Set it to 0 for the documented behaviour.
+#define OVERLAPPING 0
 static double
 form_volume(double length_a, double length_b, double length_c,
     double thick_rim_a, double thick_rim_b, double thick_rim_c)
 {
-    //return length_a * length_b * length_c;
-    return length_a * length_b * length_c +
-           2.0 * thick_rim_a * length_b * length_c +
-           2.0 * thick_rim_b * length_a * length_c +
-           2.0 * thick_rim_c * length_a * length_b;
+    return
+#if OVERLAPPING
+        // Hollow rectangular prism only includes the volume of the shell
+        // so uncomment the next line when comparing.  Solid rectangular
+        // prism, or parallelepiped want filled cores, so comment when
+        // comparing.
+        //-length_a * length_b * length_c +
+        (length_a + 2.0*thick_rim_a) *
+        (length_b + 2.0*thick_rim_b) *
+        (length_c + 2.0*thick_rim_c);
+#else
+        length_a * length_b * length_c +
+        2.0 * thick_rim_a * length_b * length_c +
+        2.0 * length_a * thick_rim_b * length_c +
+        2.0 * length_a * length_b * thick_rim_c;
+#endif
 }
 
@@ -24,90 +41,66 @@
     double thick_rim_c)
 {
-    // Code converted from functions CSPPKernel and CSParallelepiped in libCylinder.c_scaled
+    // Code converted from functions CSPPKernel and CSParallelepiped in libCylinder.c
     // Did not understand the code completely, it should be rechecked (Miguel Gonzalez)
-    //Code is rewritten,the code is compliant with Diva Singhs thesis now (Dirk Honecker)
+    // Code is rewritten, the code is compliant with Diva Singh's thesis now (Dirk Honecker)
+    // Code rewritten; cross checked against hollow rectangular prism and realspace (PAK)
 
-    const double mu = 0.5 * q * length_b;
+    const double half_q = 0.5*q;
 
-    //calculate volume before rescaling (in original code, but not used)
-    //double vol = form_volume(length_a, length_b, length_c, thick_rim_a, thick_rim_b, thick_rim_c);
-    //double vol = length_a * length_b * length_c +
-    //       2.0 * thick_rim_a * length_b * length_c +
-    //       2.0 * thick_rim_b * length_a * length_c +
-    //       2.0 * thick_rim_c * length_a * length_b;
+    const double tA = length_a + 2.0*thick_rim_a;
+    const double tB = length_b + 2.0*thick_rim_b;
+    const double tC = length_c + 2.0*thick_rim_c;
 
-    // Scale sides by B
-    const double a_scaled = length_a / length_b;
-    const double c_scaled = length_c / length_b;
-
-    double ta = a_scaled + 2.0*thick_rim_a/length_b; // incorrect ta = (a_scaled + 2.0*thick_rim_a)/length_b;
-    double tb = 1+ 2.0*thick_rim_b/length_b; // incorrect tb = (a_scaled + 2.0*thick_rim_b)/length_b;
-    double tc = c_scaled + 2.0*thick_rim_c/length_b; //not present
-
-    double Vin = length_a * length_b * length_c;
-    //double Vot = (length_a * length_b * length_c +
-    //            2.0 * thick_rim_a * length_b * length_c +
-    //            2.0 * length_a * thick_rim_b * length_c +
-    //            2.0 * length_a * length_b * thick_rim_c);
-    double V1 = (2.0 * thick_rim_a * length_b * length_c);    // incorrect V1 (aa*bb*cc+2*ta*bb*cc)
-    double V2 = (2.0 * length_a * thick_rim_b * length_c);    // incorrect V2(aa*bb*cc+2*aa*tb*cc)
-    double V3 = (2.0 * length_a * length_b * thick_rim_c);    //not present
-
-    // Scale factors (note that drC is not used later)
-    const double drho0 = (core_sld-solvent_sld);
-    const double drhoA = (arim_sld-solvent_sld);
-    const double drhoB = (brim_sld-solvent_sld);
-    const double drhoC = (crim_sld-solvent_sld);  // incorrect const double drC_Vot = (crim_sld-solvent_sld)*Vot;
-
-
-    // Precompute scale factors for combining cross terms from the shape
-    const double scale23 = drhoA*V1;
-    const double scale14 = drhoB*V2;
-    const double scale24 = drhoC*V3;
-    const double scale11 = drho0*Vin;
-    const double scale12 = drho0*Vin - scale23 - scale14 - scale24;
+    // Scale factors
+    const double dr0 = (core_sld-solvent_sld);
+    const double drA = (arim_sld-solvent_sld);
+    const double drB = (brim_sld-solvent_sld);
+    const double drC = (crim_sld-solvent_sld);
 
     // outer integral (with gauss points), integration limits = 0, 1
-    double outer_total = 0; //initialize integral
+    double outer_sum = 0; //initialize integral
+    for( int i=0; i<GAUSS_N; i++) {
+        const double cos_alpha = 0.5 * ( GAUSS_Z[i] + 1.0 );
+        const double mu = half_q * sqrt(1.0-cos_alpha*cos_alpha);
 
-    for( int i=0; i<76; i++) {
-        double sigma = 0.5 * ( Gauss76Z[i] + 1.0 );
-        double mu_proj = mu * sqrt(1.0-sigma*sigma);
+        // inner integral (with gauss points), integration limits = 0, pi/2
+        const double siC = length_c * sas_sinx_x(length_c * cos_alpha * half_q);
+        const double siCt = tC * sas_sinx_x(tC * cos_alpha * half_q);
+        double inner_sum = 0.0;
+        for(int j=0; j<GAUSS_N; j++) {
+            const double beta = 0.5 * ( GAUSS_Z[j] + 1.0 );
+            double sin_beta, cos_beta;
+            SINCOS(M_PI_2*beta, sin_beta, cos_beta);
+            const double siA = length_a * sas_sinx_x(length_a * mu * sin_beta);
+            const double siB = length_b * sas_sinx_x(length_b * mu * cos_beta);
+            const double siAt = tA * sas_sinx_x(tA * mu * sin_beta);
+            const double siBt = tB * sas_sinx_x(tB * mu * cos_beta);
 
-        // inner integral (with gauss points), integration limits = 0, 1
-        double inner_total = 0.0;
-        double inner_total_crim = 0.0;
-        for(int j=0; j<76; j++) {
-            const double uu = 0.5 * ( Gauss76Z[j] + 1.0 );
-            double sin_uu, cos_uu;
-            SINCOS(M_PI_2*uu, sin_uu, cos_uu);
-            const double si1 = sas_sinx_x(mu_proj * sin_uu * a_scaled);
-            const double si2 = sas_sinx_x(mu_proj * cos_uu );
-            const double si3 = sas_sinx_x(mu_proj * sin_uu * ta);
-            const double si4 = sas_sinx_x(mu_proj * cos_uu * tb);
+#if OVERLAPPING
+            const double f = dr0*siA*siB*siC
+                + drA*(siAt-siA)*siB*siC
+                + drB*siAt*(siBt-siB)*siC
+                + drC*siAt*siBt*(siCt-siC);
+#else
+            const double f = dr0*siA*siB*siC
+                + drA*(siAt-siA)*siB*siC
+                + drB*siA*(siBt-siB)*siC
+                + drC*siA*siB*(siCt-siC);
+#endif
 
-            // Expression in libCylinder.c (neither drC nor Vot are used)
-            const double form = scale12*si1*si2 + scale23*si2*si3 + scale14*si1*si4;
-            const double form_crim = scale11*si1*si2;
-
-            //  correct FF : sum of square of phase factors
-            inner_total += Gauss76Wt[j] * form * form;
-            inner_total_crim += Gauss76Wt[j] * form_crim * form_crim;
+            inner_sum += GAUSS_W[j] * f * f;
         }
-        inner_total *= 0.5;
-        inner_total_crim *= 0.5;
+        inner_sum *= 0.5;
         // now sum up the outer integral
-        const double si = sas_sinx_x(mu * c_scaled * sigma);
-        const double si_crim = sas_sinx_x(mu * tc * sigma);
-        outer_total += Gauss76Wt[i] * (inner_total * si * si + inner_total_crim * si_crim * si_crim);
+        outer_sum += GAUSS_W[i] * inner_sum;
     }
-    outer_total *= 0.5;
+    outer_sum *= 0.5;
 
     //convert from [1e-12 A-1] to [cm-1]
-    return 1.0e-4 * outer_total;
+    return 1.0e-4 * outer_sum;
 }
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
     double core_sld,
     double arim_sld,
@@ -128,34 +121,25 @@
     const double drC = crim_sld-solvent_sld;
 
-    double Vin = length_a * length_b * length_c;
-    double V1 = 2.0 * thick_rim_a * length_b * length_c;    // incorrect V1(aa*bb*cc+2*ta*bb*cc)
-    double V2 = 2.0 * length_a * thick_rim_b * length_c;    // incorrect V2(aa*bb*cc+2*aa*tb*cc)
-    double V3 = 2.0 * length_a * length_b * thick_rim_c;
-    // As for the 1D case, Vot is not used
-    //double Vot = (length_a * length_b * length_c +
-    //              2.0 * thick_rim_a * length_b * length_c +
-    //              2.0 * length_a * thick_rim_b * length_c +
-    //              2.0 * length_a * length_b * thick_rim_c);
+    const double tA = length_a + 2.0*thick_rim_a;
+    const double tB = length_b + 2.0*thick_rim_b;
+    const double tC = length_c + 2.0*thick_rim_c;
+    const double siA = length_a*sas_sinx_x(0.5*length_a*qa);
+    const double siB = length_b*sas_sinx_x(0.5*length_b*qb);
+    const double siC = length_c*sas_sinx_x(0.5*length_c*qc);
+    const double siAt = tA*sas_sinx_x(0.5*tA*qa);
+    const double siBt = tB*sas_sinx_x(0.5*tB*qb);
+    const double siCt = tC*sas_sinx_x(0.5*tC*qc);
 
-    // The definitions of ta, tb, tc are not the same as in the 1D case because there is no
-    // the scaling by B.
-    double ta = length_a + 2.0*thick_rim_a;
-    double tb = length_b + 2.0*thick_rim_b;
-    double tc = length_c + 2.0*thick_rim_c;
-    //handle arg=0 separately, as sin(t)/t -> 1 as t->0
-    double siA = sas_sinx_x(0.5*length_a*qa);
-    double siB = sas_sinx_x(0.5*length_b*qb);
-    double siC = sas_sinx_x(0.5*length_c*qc);
-    double siAt = sas_sinx_x(0.5*ta*qa);
-    double siBt = sas_sinx_x(0.5*tb*qb);
-    double siCt = sas_sinx_x(0.5*tc*qc);
-
-
-    // f uses Vin, V1, V2, and V3 and it seems to have more sense than the value computed
-    // in the 1D code, but should be checked!
-    double f = ( dr0*siA*siB*siC*Vin
-               + drA*(siAt-siA)*siB*siC*V1
-               + drB*siA*(siBt-siB)*siC*V2
-               + drC*siA*siB*(siCt-siC)*V3);
+#if OVERLAPPING
+    const double f = dr0*siA*siB*siC
+        + drA*(siAt-siA)*siB*siC
+        + drB*siAt*(siBt-siB)*siC
+        + drC*siAt*siBt*(siCt-siC);
+#else
+    const double f = dr0*siA*siB*siC
+        + drA*(siAt-siA)*siB*siC
+        + drB*siA*(siBt-siB)*siC
+        + drC*siA*siB*(siCt-siC);
+#endif
 
     return 1.0e-4 * f * f;
Index: sasmodels/models/core_shell_parallelepiped.py
===================================================================
--- sasmodels/models/core_shell_parallelepiped.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/models/core_shell_parallelepiped.py	(revision 97be87700f140bc9317b791ad4691cc31fa30981)
@@ -5,11 +5,5 @@
 Calculates the form factor for a rectangular solid with a core-shell structure.
 The thickness and the scattering length density of the shell or
-"rim" can be different on each (pair) of faces. However at this time the 1D
-calculation does **NOT** actually calculate a c face rim despite the presence
-of the parameter. Some other aspects of the 1D calculation may be wrong.
-
-.. note::
-   This model was originally ported from NIST IGOR macros. However, it is not
-   yet fully understood by the SasView developers and is currently under review.
+"rim" can be different on each (pair) of faces.
 
 The form factor is normalized by the particle volume $V$ such that
@@ -21,5 +15,4 @@
 where $\langle \ldots \rangle$ is an average over all possible orientations
 of the rectangular solid.
-
 
 The function calculated is the form factor of the rectangular solid below.
@@ -41,57 +34,77 @@
     V = ABC + 2t_ABC + 2t_BAC + 2t_CAB
 
-**meaning that there are "gaps" at the corners of the solid.**  Again note that
-$t_C = 0$ currently.
+**meaning that there are "gaps" at the corners of the solid.**
 
 The intensity calculated follows the :ref:`parallelepiped` model, with the
 core-shell intensity being calculated as the square of the sum of the
-amplitudes of the core and shell, in the same manner as a core-shell model.
-
-.. math::
-
-    F_{a}(Q,\alpha,\beta)=
-    \left[\frac{\sin(\tfrac{1}{2}Q(L_A+2t_A)\sin\alpha \sin\beta)
-               }{\tfrac{1}{2}Q(L_A+2t_A)\sin\alpha\sin\beta}
-    - \frac{\sin(\tfrac{1}{2}QL_A\sin\alpha \sin\beta)
-           }{\tfrac{1}{2}QL_A\sin\alpha \sin\beta} \right]
-    \left[\frac{\sin(\tfrac{1}{2}QL_B\sin\alpha \sin\beta)
-               }{\tfrac{1}{2}QL_B\sin\alpha \sin\beta} \right]
-    \left[\frac{\sin(\tfrac{1}{2}QL_C\sin\alpha \sin\beta)
-               }{\tfrac{1}{2}QL_C\sin\alpha \sin\beta} \right]
-
-.. note::
-
-    Why does t_B not appear in the above equation?
-    For the calculation of the form factor to be valid, the sides of the solid
-    MUST (perhaps not any more?) be chosen such that** $A < B < C$.
-    If this inequality is not satisfied, the model will not report an error,
-    but the calculation will not be correct and thus the result wrong.
+amplitudes of the core and the slabs on the edges.
+
+the scattering amplitude is computed for a particular orientation of the
+core-shell parallelepiped with respect to the scattering vector and then
+averaged over all possible orientations, where $\alpha$ is the angle between
+the $z$ axis and the $C$ axis of the parallelepiped, $\beta$ is
+the angle between projection of the particle in the $xy$ detector plane
+and the $y$ axis.
+
+.. math::
+
+    F(Q)
+    &= (\rho_\text{core}-\rho_\text{solvent})
+       S(Q_A, A) S(Q_B, B) S(Q_C, C) \\
+    &+ (\rho_\text{A}-\rho_\text{solvent})
+        \left[S(Q_A, A+2t_A) - S(Q_A, Q)\right] S(Q_B, B) S(Q_C, C) \\
+    &+ (\rho_\text{B}-\rho_\text{solvent})
+        S(Q_A, A) \left[S(Q_B, B+2t_B) - S(Q_B, B)\right] S(Q_C, C) \\
+    &+ (\rho_\text{C}-\rho_\text{solvent})
+        S(Q_A, A) S(Q_B, B) \left[S(Q_C, C+2t_C) - S(Q_C, C)\right]
+
+with
+
+.. math::
+
+    S(Q, L) = L \frac{\sin \tfrac{1}{2} Q L}{\tfrac{1}{2} Q L}
+
+and
+
+.. math::
+
+    Q_A &= \sin\alpha \sin\beta \\
+    Q_B &= \sin\alpha \cos\beta \\
+    Q_C &= \cos\alpha
+
+
+where $\rho_\text{core}$, $\rho_\text{A}$, $\rho_\text{B}$ and $\rho_\text{C}$
+are the scattering length of the parallelepiped core, and the rectangular
+slabs of thickness $t_A$, $t_B$ and $t_C$, respectively. $\rho_\text{solvent}$
+is the scattering length of the solvent.
 
 FITTING NOTES
+~~~~~~~~~~~~~
+
 If the scale is set equal to the particle volume fraction, $\phi$, the returned
-value is the scattered intensity per unit volume, $I(q) = \phi P(q)$.
-However, **no interparticle interference effects are included in this
-calculation.**
+value is the scattered intensity per unit volume, $I(q) = \phi P(q)$. However,
+**no interparticle interference effects are included in this calculation.**
 
 There are many parameters in this model. Hold as many fixed as possible with
 known values, or you will certainly end up at a solution that is unphysical.
 
-Constraints must be applied during fitting to ensure that the inequality
-$A < B < C$ is not violated. The calculation will not report an error,
-but the results will not be correct.
-
 The returned value is in units of |cm^-1|, on absolute scale.
 
 NB: The 2nd virial coefficient of the core_shell_parallelepiped is calculated
 based on the the averaged effective radius $(=\sqrt{(A+2t_A)(B+2t_B)/\pi})$
-and length $(C+2t_C)$ values, after appropriately
-sorting the three dimensions to give an oblate or prolate particle, to give an
-effective radius, for $S(Q)$ when $P(Q) * S(Q)$ is applied.
+and length $(C+2t_C)$ values, after appropriately sorting the three dimensions
+to give an oblate or prolate particle, to give an effective radius,
+for $S(Q)$ when $P(Q) * S(Q)$ is applied.
 
 For 2d data the orientation of the particle is required, described using
-angles $\theta$, $\phi$ and $\Psi$ as in the diagrams below, for further details
-of the calculation and angular dispersions see :ref:`orientation` .
+angles $\theta$, $\phi$ and $\Psi$ as in the diagrams below, for further
+details of the calculation and angular dispersions see :ref:`orientation`.
 The angle $\Psi$ is the rotational angle around the *long_c* axis. For example,
 $\Psi = 0$ when the *short_b* axis is parallel to the *x*-axis of the detector.
+
+For 2d, constraints must be applied during fitting to ensure that the
+inequality $A < B < C$ is not violated, and hence the correct definition
+of angles is preserved. The calculation will not report an error,
+but the results may be not correct.
 
 .. figure:: img/parallelepiped_angle_definition.png
@@ -114,5 +127,5 @@
     Equations (1), (13-14). (in German)
 .. [#] D Singh (2009). *Small angle scattering studies of self assembly in
-   lipid mixtures*, John's Hopkins University Thesis (2009) 223-225. `Available
+   lipid mixtures*, Johns Hopkins University Thesis (2009) 223-225. `Available
    from Proquest <http://search.proquest.com/docview/304915826?accountid
    =26379>`_
@@ -123,6 +136,8 @@
 * **Author:** NIST IGOR/DANSE **Date:** pre 2010
 * **Converted to sasmodels by:** Miguel Gonzales **Date:** February 26, 2016
-* **Last Modified by:** Wojciech Potrzebowski **Date:** January 11, 2017
-* **Currently Under review by:** Paul Butler
+* **Last Modified by:** Paul Kienzle **Date:** October 17, 2017
+* Cross-checked against hollow rectangular prism and rectangular prism for
+  equal thickness overlapping sides, and by Monte Carlo sampling of points
+  within the shape for non-uniform, non-overlapping sides.
 """
 
@@ -175,12 +190,10 @@
         Return equivalent radius (ER)
     """
-
-    # surface average radius (rough approximation)
-    surf_rad = sqrt((length_a + 2.0*thick_rim_a) * (length_b + 2.0*thick_rim_b) / pi)
-
-    height = length_c + 2.0*thick_rim_c
-
-    ddd = 0.75 * surf_rad * (2 * surf_rad * height + (height + surf_rad) * (height + pi * surf_rad))
-    return 0.5 * (ddd) ** (1. / 3.)
+    from .parallelepiped import ER as ER_p
+
+    a = length_a + 2*thick_rim_a
+    b = length_b + 2*thick_rim_b
+    c = length_c + 2*thick_rim_c
+    return ER_p(a, b, c)
 
 # VR defaults to 1.0
@@ -216,5 +229,6 @@
             psi_pd=10, psi_pd_n=1)
 
-# rkh 7/4/17 add random unit test for 2d, note make all params different, 2d values not tested against other codes or models
+# rkh 7/4/17 add random unit test for 2d, note make all params different,
+# 2d values not tested against other codes or models
 if 0:  # pak: model rewrite; need to update tests
     qx, qy = 0.2 * cos(pi/6.), 0.2 * sin(pi/6.)
Index: sasmodels/models/cylinder.c
===================================================================
--- sasmodels/models/cylinder.c	(revision becded3d803d05553cd978e361eeb35b6f6736bf)
+++ sasmodels/models/cylinder.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -21,11 +21,11 @@
 
     double total = 0.0;
-    for (int i=0; i<76 ;i++) {
-        const double theta = Gauss76Z[i]*zm + zb;
+    for (int i=0; i<GAUSS_N ;i++) {
+        const double theta = GAUSS_Z[i]*zm + zb;
         double sin_theta, cos_theta; // slots to hold sincos function output
         // theta (theta,phi) the projection of the cylinder on the detector plane
         SINCOS(theta , sin_theta, cos_theta);
         const double form = fq(q*sin_theta, q*cos_theta, radius, length);
-        total += Gauss76Wt[i] * form * form * sin_theta;
+        total += GAUSS_W[i] * form * form * sin_theta;
     }
     // translate dx in [-1,1] to dx in [lower,upper]
@@ -45,5 +45,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double sld,
     double solvent_sld,
Index: sasmodels/models/ellipsoid.c
===================================================================
--- sasmodels/models/ellipsoid.c	(revision becded3d803d05553cd978e361eeb35b6f6736bf)
+++ sasmodels/models/ellipsoid.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -22,13 +22,13 @@
 
     // translate a point in [-1,1] to a point in [0, 1]
-    // const double u = Gauss76Z[i]*(upper-lower)/2 + (upper+lower)/2;
+    // const double u = GAUSS_Z[i]*(upper-lower)/2 + (upper+lower)/2;
     const double zm = 0.5;
     const double zb = 0.5;
     double total = 0.0;
-    for (int i=0;i<76;i++) {
-        const double u = Gauss76Z[i]*zm + zb;
+    for (int i=0;i<GAUSS_N;i++) {
+        const double u = GAUSS_Z[i]*zm + zb;
         const double r = radius_equatorial*sqrt(1.0 + u*u*v_square_minus_one);
         const double f = sas_3j1x_x(q*r);
-        total += Gauss76Wt[i] * f * f;
+        total += GAUSS_W[i] * f * f;
     }
     // translate dx in [-1,1] to dx in [lower,upper]
@@ -39,5 +39,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double sld,
     double sld_solvent,
Index: sasmodels/models/elliptical_cylinder.c
===================================================================
--- sasmodels/models/elliptical_cylinder.c	(revision 82592da3721b7e2098cf6346cf224814189fadb3)
+++ sasmodels/models/elliptical_cylinder.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -22,16 +22,15 @@
     //initialize integral
     double outer_sum = 0.0;
-    for(int i=0;i<76;i++) {
+    for(int i=0;i<GAUSS_N;i++) {
         //setup inner integral over the ellipsoidal cross-section
-        const double cos_val = ( Gauss76Z[i]*(vb-va) + va + vb )/2.0;
+        const double cos_val = ( GAUSS_Z[i]*(vb-va) + va + vb )/2.0;
         const double sin_val = sqrt(1.0 - cos_val*cos_val);
         //const double arg = radius_minor*sin_val;
         double inner_sum=0;
-        for(int j=0;j<76;j++) {
-            //20 gauss points for the inner integral, increase to 76, RKH 6Nov2017
-            const double theta = ( Gauss76Z[j]*(vbj-vaj) + vaj + vbj )/2.0;
+        for(int j=0;j<GAUSS_N;j++) {
+            const double theta = ( GAUSS_Z[j]*(vbj-vaj) + vaj + vbj )/2.0;
             const double r = sin_val*sqrt(rA - rB*cos(theta));
             const double be = sas_2J1x_x(q*r);
-            inner_sum += Gauss76Wt[j] * be * be;
+            inner_sum += GAUSS_W[j] * be * be;
         }
         //now calculate the value of the inner integral
@@ -40,5 +39,5 @@
         //now calculate outer integral
         const double si = sas_sinx_x(q*0.5*length*cos_val);
-        outer_sum += Gauss76Wt[i] * inner_sum * si * si;
+        outer_sum += GAUSS_W[i] * inner_sum * si * si;
     }
     outer_sum *= 0.5*(vb-va);
@@ -55,5 +54,5 @@
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
      double radius_minor, double r_ratio, double length,
      double sld, double solvent_sld)
Index: sasmodels/models/elliptical_cylinder.py
===================================================================
--- sasmodels/models/elliptical_cylinder.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/models/elliptical_cylinder.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
@@ -121,6 +121,5 @@
 # pylint: enable=bad-whitespace, line-too-long
 
-source = ["lib/polevl.c", "lib/sas_J1.c", "lib/gauss76.c", "lib/gauss20.c",
-          "elliptical_cylinder.c"]
+source = ["lib/polevl.c", "lib/sas_J1.c", "lib/gauss76.c", "elliptical_cylinder.c"]
 
 demo = dict(scale=1, background=0, radius_minor=100, axis_ratio=1.5, length=400.0,
Index: sasmodels/models/fcc_paracrystal.c
===================================================================
--- sasmodels/models/fcc_paracrystal.c	(revision f728001e783d4f2cad1f52bfa5374c457648c268)
+++ sasmodels/models/fcc_paracrystal.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -53,13 +53,13 @@
 
     double outer_sum = 0.0;
-    for(int i=0; i<150; i++) {
+    for(int i=0; i<GAUSS_N; i++) {
         double inner_sum = 0.0;
-        const double theta = Gauss150Z[i]*theta_m + theta_b;
+        const double theta = GAUSS_Z[i]*theta_m + theta_b;
         double sin_theta, cos_theta;
         SINCOS(theta, sin_theta, cos_theta);
         const double qc = q*cos_theta;
         const double qab = q*sin_theta;
-        for(int j=0;j<150;j++) {
-            const double phi = Gauss150Z[j]*phi_m + phi_b;
+        for(int j=0;j<GAUSS_N;j++) {
+            const double phi = GAUSS_Z[j]*phi_m + phi_b;
             double sin_phi, cos_phi;
             SINCOS(phi, sin_phi, cos_phi);
@@ -67,8 +67,8 @@
             const double qb = qab*sin_phi;
             const double form = fcc_Zq(qa, qb, qc, dnn, d_factor);
-            inner_sum += Gauss150Wt[j] * form;
+            inner_sum += GAUSS_W[j] * form;
         }
         inner_sum *= phi_m;  // sum(f(x)dx) = sum(f(x)) dx
-        outer_sum += Gauss150Wt[i] * inner_sum * sin_theta;
+        outer_sum += GAUSS_W[i] * inner_sum * sin_theta;
     }
     outer_sum *= theta_m;
@@ -80,5 +80,5 @@
 
 
-static double Iqxy(double qa, double qb, double qc,
+static double Iqabc(double qa, double qb, double qc,
     double dnn, double d_factor, double radius,
     double sld, double solvent_sld)
Index: sasmodels/models/flexible_cylinder_elliptical.c
===================================================================
--- sasmodels/models/flexible_cylinder_elliptical.c	(revision 592343f7d47ec3aad7ffa1ca242acb1346553ede)
+++ sasmodels/models/flexible_cylinder_elliptical.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
@@ -17,11 +17,11 @@
     double sum=0.0;
 
-    for(int i=0;i<N_POINTS_76;i++) {
-        const double zi = ( Gauss76Z[i] + 1.0 )*M_PI_4;
+    for(int i=0;i<GAUSS_N;i++) {
+        const double zi = ( GAUSS_Z[i] + 1.0 )*M_PI_4;
         double sn, cn;
         SINCOS(zi, sn, cn);
         const double arg = q*sqrt(a*a*sn*sn + b*b*cn*cn);
         const double yyy = sas_2J1x_x(arg);
-        sum += Gauss76Wt[i] * yyy * yyy;
+        sum += GAUSS_W[i] * yyy * yyy;
     }
     sum *= 0.5;
Index: sasmodels/models/hollow_cylinder.c
===================================================================
--- sasmodels/models/hollow_cylinder.c	(revision becded3d803d05553cd978e361eeb35b6f6736bf)
+++ sasmodels/models/hollow_cylinder.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -38,10 +38,10 @@
 
     double summ = 0.0;            //initialize intergral
-    for (int i=0;i<76;i++) {
-        const double cos_theta = 0.5*( Gauss76Z[i] * (upper-lower) + lower + upper );
+    for (int i=0;i<GAUSS_N;i++) {
+        const double cos_theta = 0.5*( GAUSS_Z[i] * (upper-lower) + lower + upper );
         const double sin_theta = sqrt(1.0 - cos_theta*cos_theta);
         const double form = _fq(q*sin_theta, q*cos_theta,
                                 radius, thickness, length);
-        summ += Gauss76Wt[i] * form * form;
+        summ += GAUSS_W[i] * form * form;
     }
 
@@ -52,5 +52,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double radius, double thickness, double length,
     double sld, double solvent_sld)
Index: sasmodels/models/hollow_rectangular_prism.c
===================================================================
--- sasmodels/models/hollow_rectangular_prism.c	(revision 1e7b0db0b7b4315c62b59e1d659332bcbc962b8f)
+++ sasmodels/models/hollow_rectangular_prism.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -1,4 +1,4 @@
 double form_volume(double length_a, double b2a_ratio, double c2a_ratio, double thickness);
-double Iq(double q, double sld, double solvent_sld, double length_a, 
+double Iq(double q, double sld, double solvent_sld, double length_a,
           double b2a_ratio, double c2a_ratio, double thickness);
 
@@ -37,9 +37,9 @@
     const double v2a = 0.0;
     const double v2b = M_PI_2;  //phi integration limits
-    
+
     double outer_sum = 0.0;
-    for(int i=0; i<76; i++) {
+    for(int i=0; i<GAUSS_N; i++) {
 
-        const double theta = 0.5 * ( Gauss76Z[i]*(v1b-v1a) + v1a + v1b );
+        const double theta = 0.5 * ( GAUSS_Z[i]*(v1b-v1a) + v1a + v1b );
         double sin_theta, cos_theta;
         SINCOS(theta, sin_theta, cos_theta);
@@ -49,7 +49,7 @@
 
         double inner_sum = 0.0;
-        for(int j=0; j<76; j++) {
+        for(int j=0; j<GAUSS_N; j++) {
 
-            const double phi = 0.5 * ( Gauss76Z[j]*(v2b-v2a) + v2a + v2b );
+            const double phi = 0.5 * ( GAUSS_Z[j]*(v2b-v2a) + v2a + v2b );
             double sin_phi, cos_phi;
             SINCOS(phi, sin_phi, cos_phi);
@@ -66,9 +66,9 @@
             const double AP2 = vol_core * termA2 * termB2 * termC2;
 
-            inner_sum += Gauss76Wt[j] * square(AP1-AP2);
+            inner_sum += GAUSS_W[j] * square(AP1-AP2);
         }
         inner_sum *= 0.5 * (v2b-v2a);
 
-        outer_sum += Gauss76Wt[i] * inner_sum * sin(theta);
+        outer_sum += GAUSS_W[i] * inner_sum * sin(theta);
     }
     outer_sum *= 0.5*(v1b-v1a);
@@ -84,2 +84,39 @@
     return 1.0e-4 * delrho * delrho * form;
 }
+
+double Iqabc(double qa, double qb, double qc,
+    double sld,
+    double solvent_sld,
+    double length_a,
+    double b2a_ratio,
+    double c2a_ratio,
+    double thickness)
+{
+    const double length_b = length_a * b2a_ratio;
+    const double length_c = length_a * c2a_ratio;
+    const double a_half = 0.5 * length_a;
+    const double b_half = 0.5 * length_b;
+    const double c_half = 0.5 * length_c;
+    const double vol_total = length_a * length_b * length_c;
+    const double vol_core = 8.0 * (a_half-thickness) * (b_half-thickness) * (c_half-thickness);
+
+    // Amplitude AP from eqn. (13)
+
+    const double termA1 = sas_sinx_x(qa * a_half);
+    const double termA2 = sas_sinx_x(qa * (a_half-thickness));
+
+    const double termB1 = sas_sinx_x(qb * b_half);
+    const double termB2 = sas_sinx_x(qb * (b_half-thickness));
+
+    const double termC1 = sas_sinx_x(qc * c_half);
+    const double termC2 = sas_sinx_x(qc * (c_half-thickness));
+
+    const double AP1 = vol_total * termA1 * termB1 * termC1;
+    const double AP2 = vol_core * termA2 * termB2 * termC2;
+
+    // Multiply by contrast^2. Factor corresponding to volume^2 cancels with previous normalization.
+    const double delrho = sld - solvent_sld;
+
+    // Convert from [1e-12 A-1] to [cm-1]
+    return 1.0e-4 * square(delrho * (AP1-AP2));
+}
Index: sasmodels/models/hollow_rectangular_prism.py
===================================================================
--- sasmodels/models/hollow_rectangular_prism.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/models/hollow_rectangular_prism.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
@@ -5,5 +5,5 @@
 This model provides the form factor, $P(q)$, for a hollow rectangular
 parallelepiped with a wall of thickness $\Delta$.
-It computes only the 1D scattering, not the 2D.
+
 
 Definition
@@ -66,5 +66,25 @@
 (which is unitless).
 
-**The 2D scattering intensity is not computed by this model.**
+For 2d data the orientation of the particle is required, described using
+angles $\theta$, $\phi$ and $\Psi$ as in the diagrams below, for further details
+of the calculation and angular dispersions see :ref:`orientation` .
+The angle $\Psi$ is the rotational angle around the long *C* axis. For example,
+$\Psi = 0$ when the *B* axis is parallel to the *x*-axis of the detector.
+
+For 2d, constraints must be applied during fitting to ensure that the inequality
+$A < B < C$ is not violated, and hence the correct definition of angles is preserved. The calculation will not report an error,
+but the results may be not correct.
+
+.. figure:: img/parallelepiped_angle_definition.png
+
+    Definition of the angles for oriented hollow rectangular prism.
+    Note that rotation $\theta$, initially in the $xz$ plane, is carried out first, then
+    rotation $\phi$ about the $z$ axis, finally rotation $\Psi$ is now around the axis of the prism.
+    The neutron or X-ray beam is along the $z$ axis.
+
+.. figure:: img/parallelepiped_angle_projection.png
+
+    Examples of the angles for oriented hollow rectangular prisms against the
+    detector plane.
 
 
@@ -113,4 +133,10 @@
               ["thickness", "Ang", 1, [0, inf], "volume",
                "Thickness of parallelepiped"],
+              ["theta", "degrees", 0, [-360, 360], "orientation",
+               "c axis to beam angle"],
+              ["phi", "degrees", 0, [-360, 360], "orientation",
+               "rotation about beam"],
+              ["psi", "degrees", 0, [-360, 360], "orientation",
+               "rotation about c axis"],
              ]
 
Index: sasmodels/models/hollow_rectangular_prism_thin_walls.c
===================================================================
--- sasmodels/models/hollow_rectangular_prism_thin_walls.c	(revision ab2aea81f7fb392e5f24f1c76dd989914d8f7022)
+++ sasmodels/models/hollow_rectangular_prism_thin_walls.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
@@ -1,4 +1,4 @@
 double form_volume(double length_a, double b2a_ratio, double c2a_ratio);
-double Iq(double q, double sld, double solvent_sld, double length_a, 
+double Iq(double q, double sld, double solvent_sld, double length_a,
           double b2a_ratio, double c2a_ratio);
 
@@ -29,8 +29,8 @@
     const double v2a = 0.0;
     const double v2b = M_PI_2;  //phi integration limits
-    
+
     double outer_sum = 0.0;
-    for(int i=0; i<76; i++) {
-        const double theta = 0.5 * ( Gauss76Z[i]*(v1b-v1a) + v1a + v1b );
+    for(int i=0; i<GAUSS_N; i++) {
+        const double theta = 0.5 * ( GAUSS_Z[i]*(v1b-v1a) + v1a + v1b );
 
         double sin_theta, cos_theta;
@@ -44,6 +44,6 @@
 
         double inner_sum = 0.0;
-        for(int j=0; j<76; j++) {
-            const double phi = 0.5 * ( Gauss76Z[j]*(v2b-v2a) + v2a + v2b );
+        for(int j=0; j<GAUSS_N; j++) {
+            const double phi = 0.5 * ( GAUSS_Z[j]*(v2b-v2a) + v2a + v2b );
 
             double sin_phi, cos_phi;
@@ -62,9 +62,9 @@
                 * ( cos_a*sin_b/cos_phi + cos_b*sin_a/sin_phi );
 
-            inner_sum += Gauss76Wt[j] * square(AL+AT);
+            inner_sum += GAUSS_W[j] * square(AL+AT);
         }
 
         inner_sum *= 0.5 * (v2b-v2a);
-        outer_sum += Gauss76Wt[i] * inner_sum * sin_theta;
+        outer_sum += GAUSS_W[i] * inner_sum * sin_theta;
     }
 
Index: sasmodels/models/lib/gauss150.c
===================================================================
--- sasmodels/models/lib/gauss150.c	(revision 994d77f9b713691e41106d1f9af9661dfb2dc419)
+++ sasmodels/models/lib/gauss150.c	(revision 99b84ec0c1c8127b9cbe4beeecc251208c44071c)
@@ -1,13 +1,17 @@
-/*
- *  GaussWeights.c
- *  SANSAnalysis
- *
- *  Created by Andrew Jackson on 4/23/07.
- *  Copyright 2007 __MyCompanyName__. All rights reserved.
- *
- */
+// Created by Andrew Jackson on 4/23/07
 
-// Gaussians
-constant double Gauss150Z[150]={
+ #ifdef GAUSS_N
+ # undef GAUSS_N
+ # undef GAUSS_Z
+ # undef GAUSS_W
+ #endif
+ #define GAUSS_N 150
+ #define GAUSS_Z Gauss150Z
+ #define GAUSS_W Gauss150Wt
+
+
+// Note: using array size 152 rather than 150 so that it is a multiple of 4.
+// Some OpenCL devices prefer that vectors start and end on nice boundaries.
+constant double Gauss150Z[152]={
   	-0.9998723404457334,
   	-0.9993274305065947,
@@ -159,8 +163,10 @@
   	0.9983473449340834,
   	0.9993274305065947,
-  	0.9998723404457334
+  	0.9998723404457334,
+  	0., // zero padding is ignored
+  	0.  // zero padding is ignored
 };
 
-constant double Gauss150Wt[150]={
+constant double Gauss150Wt[152]={
   	0.0003276086705538,
   	0.0007624720924706,
@@ -312,4 +318,6 @@
   	0.0011976474864367,
   	0.0007624720924706,
-  	0.0003276086705538
+  	0.0003276086705538,
+  	0., // zero padding is ignored
+  	0.  // zero padding is ignored
 };
Index: sasmodels/models/lib/gauss20.c
===================================================================
--- sasmodels/models/lib/gauss20.c	(revision 994d77f9b713691e41106d1f9af9661dfb2dc419)
+++ sasmodels/models/lib/gauss20.c	(revision 99b84ec0c1c8127b9cbe4beeecc251208c44071c)
@@ -1,10 +1,12 @@
-/*
- *  GaussWeights.c
- *  SANSAnalysis
- *
- *  Created by Andrew Jackson on 4/23/07.
- *  Copyright 2007 __MyCompanyName__. All rights reserved.
- *
- */
+// Created by Andrew Jackson on 4/23/07
+
+ #ifdef GAUSS_N
+ # undef GAUSS_N
+ # undef GAUSS_Z
+ # undef GAUSS_W
+ #endif
+ #define GAUSS_N 20
+ #define GAUSS_Z Gauss20Z
+ #define GAUSS_W Gauss20Wt
 
 // Gaussians
Index: sasmodels/models/lib/gauss76.c
===================================================================
--- sasmodels/models/lib/gauss76.c	(revision 66d119fbbe121774089e17e3086b4809c9cf2a80)
+++ sasmodels/models/lib/gauss76.c	(revision 99b84ec0c1c8127b9cbe4beeecc251208c44071c)
@@ -1,14 +1,15 @@
-/*
- *  GaussWeights.c
- *  SANSAnalysis
- *
- *  Created by Andrew Jackson on 4/23/07.
- *  Copyright 2007 __MyCompanyName__. All rights reserved.
- *
- */
-#define N_POINTS_76 76
+// Created by Andrew Jackson on 4/23/07
+
+ #ifdef GAUSS_N
+ # undef GAUSS_N
+ # undef GAUSS_Z
+ # undef GAUSS_W
+ #endif
+ #define GAUSS_N 76
+ #define GAUSS_Z Gauss76Z
+ #define GAUSS_W Gauss76Wt
 
 // Gaussians
-constant double Gauss76Wt[N_POINTS_76]={
+constant double Gauss76Wt[76]={
 	.00126779163408536,		//0
 	.00294910295364247,
@@ -89,5 +90,5 @@
 };
 
-constant double Gauss76Z[N_POINTS_76]={
+constant double Gauss76Z[76]={
 	-.999505948362153,		//0
 	-.997397786355355,
Index: sasmodels/models/line.py
===================================================================
--- sasmodels/models/line.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/models/line.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -57,4 +57,5 @@
 Iq.vectorized = True # Iq accepts an array of q values
 
+
 def Iqxy(qx, qy, *args):
     """
@@ -69,4 +70,12 @@
 
 Iqxy.vectorized = True  # Iqxy accepts an array of qx qy values
+
+# uncomment the following to test Iqxy in C models
+#del Iq, Iqxy
+#c_code = """
+#static double Iq(double q, double b, double m) { return m*q+b; }
+#static double Iqxy(double qx, double qy, double b, double m)
+#{ return (m*qx+b)*(m*qy+b); }
+#"""
 
 def random():
Index: sasmodels/models/parallelepiped.c
===================================================================
--- sasmodels/models/parallelepiped.c	(revision 9b7b23fffe93c3cd39b094e9e17d0bf62c39b2bc)
+++ sasmodels/models/parallelepiped.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -23,6 +23,6 @@
     double outer_total = 0; //initialize integral
 
-    for( int i=0; i<76; i++) {
-        const double sigma = 0.5 * ( Gauss76Z[i] + 1.0 );
+    for( int i=0; i<GAUSS_N; i++) {
+        const double sigma = 0.5 * ( GAUSS_Z[i] + 1.0 );
         const double mu_proj = mu * sqrt(1.0-sigma*sigma);
 
@@ -30,16 +30,16 @@
         // corresponding to angles from 0 to pi/2.
         double inner_total = 0.0;
-        for(int j=0; j<76; j++) {
-            const double uu = 0.5 * ( Gauss76Z[j] + 1.0 );
+        for(int j=0; j<GAUSS_N; j++) {
+            const double uu = 0.5 * ( GAUSS_Z[j] + 1.0 );
             double sin_uu, cos_uu;
             SINCOS(M_PI_2*uu, sin_uu, cos_uu);
             const double si1 = sas_sinx_x(mu_proj * sin_uu * a_scaled);
             const double si2 = sas_sinx_x(mu_proj * cos_uu);
-            inner_total += Gauss76Wt[j] * square(si1 * si2);
+            inner_total += GAUSS_W[j] * square(si1 * si2);
         }
         inner_total *= 0.5;
 
         const double si = sas_sinx_x(mu * c_scaled * sigma);
-        outer_total += Gauss76Wt[i] * inner_total * si * si;
+        outer_total += GAUSS_W[i] * inner_total * si * si;
     }
     outer_total *= 0.5;
@@ -53,5 +53,5 @@
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
     double sld,
     double solvent_sld,
Index: sasmodels/models/pringle.c
===================================================================
--- sasmodels/models/pringle.c	(revision 1e7b0db0b7b4315c62b59e1d659332bcbc962b8f)
+++ sasmodels/models/pringle.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
@@ -29,11 +29,11 @@
     double sumC = 0.0;		// initialize integral
     double r;
-    for (int i=0; i < 76; i++) {
-        r = Gauss76Z[i]*zm + zb;
+    for (int i=0; i < GAUSS_N; i++) {
+        r = GAUSS_Z[i]*zm + zb;
 
         const double qrs = r*q_sin_psi;
         const double qrrc = r*r*q_cos_psi;
 
-        double y = Gauss76Wt[i] * r * sas_JN(n, beta*qrrc) * sas_JN(2*n, qrs);
+        double y = GAUSS_W[i] * r * sas_JN(n, beta*qrrc) * sas_JN(2*n, qrs);
         double S, C;
         SINCOS(alpha*qrrc, S, C);
@@ -86,6 +86,6 @@
 
     double sum = 0.0;
-    for (int i = 0; i < 76; i++) {
-        double psi = Gauss76Z[i]*zm + zb;
+    for (int i = 0; i < GAUSS_N; i++) {
+        double psi = GAUSS_Z[i]*zm + zb;
         double sin_psi, cos_psi;
         SINCOS(psi, sin_psi, cos_psi);
@@ -93,5 +93,5 @@
         double sinc_term = square(sas_sinx_x(q * thickness * cos_psi / 2.0));
         double pringle_kernel = 4.0 * sin_psi * bessel_term * sinc_term;
-        sum += Gauss76Wt[i] * pringle_kernel;
+        sum += GAUSS_W[i] * pringle_kernel;
     }
 
Index: sasmodels/models/rectangular_prism.c
===================================================================
--- sasmodels/models/rectangular_prism.c	(revision 1e7b0db0b7b4315c62b59e1d659332bcbc962b8f)
+++ sasmodels/models/rectangular_prism.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -1,4 +1,4 @@
 double form_volume(double length_a, double b2a_ratio, double c2a_ratio);
-double Iq(double q, double sld, double solvent_sld, double length_a, 
+double Iq(double q, double sld, double solvent_sld, double length_a,
           double b2a_ratio, double c2a_ratio);
 
@@ -26,8 +26,8 @@
     const double v2a = 0.0;
     const double v2b = M_PI_2;  //phi integration limits
-    
+
     double outer_sum = 0.0;
-    for(int i=0; i<76; i++) {
-        const double theta = 0.5 * ( Gauss76Z[i]*(v1b-v1a) + v1a + v1b );
+    for(int i=0; i<GAUSS_N; i++) {
+        const double theta = 0.5 * ( GAUSS_Z[i]*(v1b-v1a) + v1a + v1b );
         double sin_theta, cos_theta;
         SINCOS(theta, sin_theta, cos_theta);
@@ -36,6 +36,6 @@
 
         double inner_sum = 0.0;
-        for(int j=0; j<76; j++) {
-            double phi = 0.5 * ( Gauss76Z[j]*(v2b-v2a) + v2a + v2b );
+        for(int j=0; j<GAUSS_N; j++) {
+            double phi = 0.5 * ( GAUSS_Z[j]*(v2b-v2a) + v2a + v2b );
             double sin_phi, cos_phi;
             SINCOS(phi, sin_phi, cos_phi);
@@ -45,16 +45,16 @@
             const double termB = sas_sinx_x(q * b_half * sin_theta * cos_phi);
             const double AP = termA * termB * termC;
-            inner_sum += Gauss76Wt[j] * AP * AP;
+            inner_sum += GAUSS_W[j] * AP * AP;
         }
         inner_sum = 0.5 * (v2b-v2a) * inner_sum;
-        outer_sum += Gauss76Wt[i] * inner_sum * sin_theta;
+        outer_sum += GAUSS_W[i] * inner_sum * sin_theta;
     }
 
     double answer = 0.5*(v1b-v1a)*outer_sum;
 
-    // Normalize by Pi (Eqn. 16). 
-    // The term (ABC)^2 does not appear because it was introduced before on 
+    // Normalize by Pi (Eqn. 16).
+    // The term (ABC)^2 does not appear because it was introduced before on
     // the definitions of termA, termB, termC.
-    // The factor 2 appears because the theta integral has been defined between 
+    // The factor 2 appears because the theta integral has been defined between
     // 0 and pi/2, instead of 0 to pi.
     answer /= M_PI_2; //Form factor P(q)
@@ -64,7 +64,37 @@
     answer *= square((sld-solvent_sld)*volume);
 
-    // Convert from [1e-12 A-1] to [cm-1] 
+    // Convert from [1e-12 A-1] to [cm-1]
     answer *= 1.0e-4;
 
     return answer;
 }
+
+
+double Iqabc(double qa, double qb, double qc,
+    double sld,
+    double solvent_sld,
+    double length_a,
+    double b2a_ratio,
+    double c2a_ratio)
+{
+    const double length_b = length_a * b2a_ratio;
+    const double length_c = length_a * c2a_ratio;
+    const double a_half = 0.5 * length_a;
+    const double b_half = 0.5 * length_b;
+    const double c_half = 0.5 * length_c;
+    const double volume = length_a * length_b * length_c;
+
+    // Amplitude AP from eqn. (13)
+
+    const double termA = sas_sinx_x(qa * a_half);
+    const double termB = sas_sinx_x(qb * b_half);
+    const double termC = sas_sinx_x(qc * c_half);
+
+    const double AP = termA * termB * termC;
+
+    // Multiply by contrast^2. Factor corresponding to volume^2 cancels with previous normalization.
+    const double delrho = sld - solvent_sld;
+
+    // Convert from [1e-12 A-1] to [cm-1]
+    return 1.0e-4 * square(volume * delrho * AP);
+}
Index: sasmodels/models/rectangular_prism.py
===================================================================
--- sasmodels/models/rectangular_prism.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/models/rectangular_prism.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
@@ -12,6 +12,4 @@
 the prism (e.g. setting $b/a = 1$ and $c/a = 1$ and applying polydispersity
 to *a* will generate a distribution of cubes of different sizes).
-Note also that, contrary to :ref:`parallelepiped`, it does not compute
-the 2D scattering.
 
 
@@ -26,6 +24,5 @@
 that reference), with $\theta$ corresponding to $\alpha$ in that paper,
 and not to the usual convention used for example in the
-:ref:`parallelepiped` model. As the present model does not compute
-the 2D scattering, this has no further consequences.
+:ref:`parallelepiped` model.
 
 In this model the scattering from a massive parallelepiped with an
@@ -65,5 +62,26 @@
 units) *scale* represents the volume fraction (which is unitless).
 
-**The 2D scattering intensity is not computed by this model.**
+For 2d data the orientation of the particle is required, described using
+angles $\theta$, $\phi$ and $\Psi$ as in the diagrams below, for further details
+of the calculation and angular dispersions see :ref:`orientation` .
+The angle $\Psi$ is the rotational angle around the long *C* axis. For example,
+$\Psi = 0$ when the *B* axis is parallel to the *x*-axis of the detector.
+
+For 2d, constraints must be applied during fitting to ensure that the inequality
+$A < B < C$ is not violated, and hence the correct definition of angles is preserved. The calculation will not report an error,
+but the results may be not correct.
+
+.. figure:: img/parallelepiped_angle_definition.png
+
+    Definition of the angles for oriented core-shell parallelepipeds.
+    Note that rotation $\theta$, initially in the $xz$ plane, is carried out first, then
+    rotation $\phi$ about the $z$ axis, finally rotation $\Psi$ is now around the axis of the cylinder.
+    The neutron or X-ray beam is along the $z$ axis.
+
+.. figure:: img/parallelepiped_angle_projection.png
+
+    Examples of the angles for oriented rectangular prisms against the
+    detector plane.
+
 
 
@@ -108,4 +126,10 @@
               ["c2a_ratio", "", 1, [0, inf], "volume",
                "Ratio sides c/a"],
+              ["theta", "degrees", 0, [-360, 360], "orientation",
+               "c axis to beam angle"],
+              ["phi", "degrees", 0, [-360, 360], "orientation",
+               "rotation about beam"],
+              ["psi", "degrees", 0, [-360, 360], "orientation",
+               "rotation about c axis"],
              ]
 
Index: sasmodels/models/sc_paracrystal.c
===================================================================
--- sasmodels/models/sc_paracrystal.c	(revision f728001e783d4f2cad1f52bfa5374c457648c268)
+++ sasmodels/models/sc_paracrystal.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -54,13 +54,13 @@
 
     double outer_sum = 0.0;
-    for(int i=0; i<150; i++) {
+    for(int i=0; i<GAUSS_N; i++) {
         double inner_sum = 0.0;
-        const double theta = Gauss150Z[i]*theta_m + theta_b;
+        const double theta = GAUSS_Z[i]*theta_m + theta_b;
         double sin_theta, cos_theta;
         SINCOS(theta, sin_theta, cos_theta);
         const double qc = q*cos_theta;
         const double qab = q*sin_theta;
-        for(int j=0;j<150;j++) {
-            const double phi = Gauss150Z[j]*phi_m + phi_b;
+        for(int j=0;j<GAUSS_N;j++) {
+            const double phi = GAUSS_Z[j]*phi_m + phi_b;
             double sin_phi, cos_phi;
             SINCOS(phi, sin_phi, cos_phi);
@@ -68,8 +68,8 @@
             const double qb = qab*sin_phi;
             const double form = sc_Zq(qa, qb, qc, dnn, d_factor);
-            inner_sum += Gauss150Wt[j] * form;
+            inner_sum += GAUSS_W[j] * form;
         }
         inner_sum *= phi_m;  // sum(f(x)dx) = sum(f(x)) dx
-        outer_sum += Gauss150Wt[i] * inner_sum * sin_theta;
+        outer_sum += GAUSS_W[i] * inner_sum * sin_theta;
     }
     outer_sum *= theta_m;
@@ -82,5 +82,5 @@
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
     double dnn, double d_factor, double radius,
     double sld, double solvent_sld)
Index: sasmodels/models/stacked_disks.c
===================================================================
--- sasmodels/models/stacked_disks.c	(revision becded3d803d05553cd978e361eeb35b6f6736bf)
+++ sasmodels/models/stacked_disks.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -81,6 +81,6 @@
     double halfheight = 0.5*thick_core;
 
-    for(int i=0; i<N_POINTS_76; i++) {
-        double zi = (Gauss76Z[i] + 1.0)*M_PI_4;
+    for(int i=0; i<GAUSS_N; i++) {
+        double zi = (GAUSS_Z[i] + 1.0)*M_PI_4;
         double sin_alpha, cos_alpha; // slots to hold sincos function output
         SINCOS(zi, sin_alpha, cos_alpha);
@@ -95,5 +95,5 @@
                            solvent_sld,
                            d);
-        summ += Gauss76Wt[i] * yyy * sin_alpha;
+        summ += GAUSS_W[i] * yyy * sin_alpha;
     }
 
@@ -142,5 +142,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double thick_core,
     double thick_layer,
Index: sasmodels/models/triaxial_ellipsoid.c
===================================================================
--- sasmodels/models/triaxial_ellipsoid.c	(revision becded3d803d05553cd978e361eeb35b6f6736bf)
+++ sasmodels/models/triaxial_ellipsoid.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -21,7 +21,7 @@
     const double zb = M_PI_4;
     double outer = 0.0;
-    for (int i=0;i<76;i++) {
-        //const double u = Gauss76Z[i]*(upper-lower)/2 + (upper + lower)/2;
-        const double phi = Gauss76Z[i]*zm + zb;
+    for (int i=0;i<GAUSS_N;i++) {
+        //const double u = GAUSS_Z[i]*(upper-lower)/2 + (upper + lower)/2;
+        const double phi = GAUSS_Z[i]*zm + zb;
         const double pa_sinsq_phi = pa*square(sin(phi));
 
@@ -29,12 +29,12 @@
         const double um = 0.5;
         const double ub = 0.5;
-        for (int j=0;j<76;j++) {
+        for (int j=0;j<GAUSS_N;j++) {
             // translate a point in [-1,1] to a point in [0, 1]
-            const double usq = square(Gauss76Z[j]*um + ub);
+            const double usq = square(GAUSS_Z[j]*um + ub);
             const double r = radius_equat_major*sqrt(pa_sinsq_phi*(1.0-usq) + 1.0 + pc*usq);
             const double fq = sas_3j1x_x(q*r);
-            inner += Gauss76Wt[j] * fq * fq;
+            inner += GAUSS_W[j] * fq * fq;
         }
-        outer += Gauss76Wt[i] * inner;  // correcting for dx later
+        outer += GAUSS_W[i] * inner;  // correcting for dx later
     }
     // translate integration ranges from [-1,1] to [lower,upper] and normalize by 4 pi
@@ -46,5 +46,5 @@
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
     double sld,
     double sld_solvent,
Index: sasmodels/special.py
===================================================================
--- sasmodels/special.py	(revision e65c3ba5944b729f651cf8747f7b64bed2cbb826)
+++ sasmodels/special.py	(revision df69efa2cdb726d21ce081492d47b44552fc0d19)
@@ -3,16 +3,13 @@
 .................
 
-The C code follows the C99 standard, with the usual math functions,
-as defined in
-`OpenCL <https://www.khronos.org/registry/cl/sdk/1.1/docs/man/xhtml/mathFunctions.html>`_.
-This includes the following:
+This following standard C99 math functions are available:
 
     M_PI, M_PI_2, M_PI_4, M_SQRT1_2, M_E:
         $\pi$, $\pi/2$, $\pi/4$, $1/\sqrt{2}$ and Euler's constant $e$
 
-    exp, log, pow(x,y), expm1, sqrt:
-        Power functions $e^x$, $\ln x$, $x^y$, $e^x - 1$, $\sqrt{x}$.
-        The function expm1(x) is accurate across all $x$, including $x$
-        very close to zero.
+    exp, log, pow(x,y), expm1, log1p, sqrt, cbrt:
+        Power functions $e^x$, $\ln x$, $x^y$, $e^x - 1$, $\ln 1 + x$,
+        $\sqrt{x}$, $\sqrt[3]{x}$. The functions expm1(x) and log1p(x)
+        are accurate across all $x$, including $x$ very close to zero.
 
     sin, cos, tan, asin, acos, atan:
@@ -29,5 +26,5 @@
         quadrants II and IV when $x$ and $y$ have opposite sign.
 
-    fmin(x,y), fmax(x,y), trunc, rint:
+    fabs(x), fmin(x,y), fmax(x,y), trunc, rint:
         Floating point functions.  rint(x) returns the nearest integer.
 
@@ -35,5 +32,6 @@
         NaN, Not a Number, $0/0$.  Use isnan(x) to test for NaN.  Note that
         you cannot use :code:`x == NAN` to test for NaN values since that
-        will always return false.  NAN does not equal NAN!
+        will always return false.  NAN does not equal NAN!  The alternative,
+        :code:`x != x` may fail if the compiler optimizes the test away.
 
     INFINITY:
@@ -89,6 +87,5 @@
         for forcing a constant to stay double precision.
 
-The following special functions and scattering calculations are defined in
-`sasmodels/models/lib <https://github.com/SasView/sasmodels/tree/master/sasmodels/models/lib>`_.
+The following special functions and scattering calculations are defined.
 These functions have been tuned to be fast and numerically stable down
 to $q=0$ even in single precision.  In some cases they work around bugs
@@ -184,11 +181,14 @@
 
 
-    Gauss76Z[i], Gauss76Wt[i]:
+    gauss76.n, gauss76.z[i], gauss76.w[i]:
         Points $z_i$ and weights $w_i$ for 76-point Gaussian quadrature, respectively,
         computing $\int_{-1}^1 f(z)\,dz \approx \sum_{i=1}^{76} w_i\,f(z_i)$.
-
-        Similar arrays are available in :code:`gauss20.c` for 20-point
-        quadrature and in :code:`gauss150.c` for 150-point quadrature.
-
+        When translating the model to C, include 'lib/gauss76.c' in the source
+        and use :code:`GAUSS_N`, :code:`GAUSS_Z`, and :code:`GAUSS_W`.
+
+        Similar arrays are available in :code:`gauss20` for 20-point quadrature
+        and :code:`gauss150.c` for 150-point quadrature. By using
+        :code:`import gauss76 as gauss` it is easy to change the number of
+        points in the integration.
 """
 # pylint: disable=unused-import
@@ -200,11 +200,10 @@
 
 # C99 standard math library functions
-from numpy import exp, log, power as pow, expm1, sqrt
+from numpy import exp, log, power as pow, expm1, log1p, sqrt, cbrt
 from numpy import sin, cos, tan, arcsin as asin, arccos as acos, arctan as atan
 from numpy import sinh, cosh, tanh, arcsinh as asinh, arccosh as acosh, arctanh as atanh
 from numpy import arctan2 as atan2
-from numpy import fmin, fmax, trunc, rint
-from numpy import NAN, inf as INFINITY
-
+from numpy import fabs, fmin, fmax, trunc, rint
+from numpy import pi, nan, inf
 from scipy.special import gamma as sas_gamma
 from scipy.special import erf as sas_erf
@@ -218,4 +217,6 @@
 # C99 standard math constants
 M_PI, M_PI_2, M_PI_4, M_SQRT1_2, M_E = np.pi, np.pi/2, np.pi/4, np.sqrt(0.5), np.e
+NAN = nan
+INFINITY = inf
 
 # non-standard constants
@@ -226,4 +227,5 @@
     """return sin(x), cos(x)"""
     return sin(x), cos(x)
+sincos = SINCOS
 
 def square(x):
@@ -294,512 +296,520 @@
 
 # Gaussians
-
-Gauss20Wt = np.array([
-    .0176140071391521,
-    .0406014298003869,
-    .0626720483341091,
-    .0832767415767047,
-    .10193011981724,
-    .118194531961518,
-    .131688638449177,
-    .142096109318382,
-    .149172986472604,
-    .152753387130726,
-    .152753387130726,
-    .149172986472604,
-    .142096109318382,
-    .131688638449177,
-    .118194531961518,
-    .10193011981724,
-    .0832767415767047,
-    .0626720483341091,
-    .0406014298003869,
-    .0176140071391521
-])
-
-Gauss20Z = np.array([
-    -.993128599185095,
-    -.963971927277914,
-    -.912234428251326,
-    -.839116971822219,
-    -.746331906460151,
-    -.636053680726515,
-    -.510867001950827,
-    -.37370608871542,
-    -.227785851141645,
-    -.076526521133497,
-    .0765265211334973,
-    .227785851141645,
-    .37370608871542,
-    .510867001950827,
-    .636053680726515,
-    .746331906460151,
-    .839116971822219,
-    .912234428251326,
-    .963971927277914,
-    .993128599185095
-])
-
-Gauss76Wt = np.array([
-    .00126779163408536,		#0
-    .00294910295364247,
-    .00462793522803742,
-    .00629918049732845,
-    .00795984747723973,
-    .00960710541471375,
-    .0112381685696677,
-    .0128502838475101,
-    .0144407317482767,
-    .0160068299122486,
-    .0175459372914742,		#10
-    .0190554584671906,
-    .020532847967908,
-    .0219756145344162,
-    .0233813253070112,
-    .0247476099206597,
-    .026072164497986,
-    .0273527555318275,
-    .028587223650054,
-    .029773487255905,
-    .0309095460374916,		#20
-    .0319934843404216,
-    .0330234743977917,
-    .0339977794120564,
-    .0349147564835508,
-    .0357728593807139,
-    .0365706411473296,
-    .0373067565423816,
-    .0379799643084053,
-    .0385891292645067,
-    .0391332242205184,		#30
-    .0396113317090621,
-    .0400226455325968,
-    .040366472122844,
-    .0406422317102947,
-    .0408494593018285,
-    .040987805464794,
-    .0410570369162294,
-    .0410570369162294,
-    .040987805464794,
-    .0408494593018285,		#40
-    .0406422317102947,
-    .040366472122844,
-    .0400226455325968,
-    .0396113317090621,
-    .0391332242205184,
-    .0385891292645067,
-    .0379799643084053,
-    .0373067565423816,
-    .0365706411473296,
-    .0357728593807139,		#50
-    .0349147564835508,
-    .0339977794120564,
-    .0330234743977917,
-    .0319934843404216,
-    .0309095460374916,
-    .029773487255905,
-    .028587223650054,
-    .0273527555318275,
-    .026072164497986,
-    .0247476099206597,		#60
-    .0233813253070112,
-    .0219756145344162,
-    .020532847967908,
-    .0190554584671906,
-    .0175459372914742,
-    .0160068299122486,
-    .0144407317482767,
-    .0128502838475101,
-    .0112381685696677,
-    .00960710541471375,		#70
-    .00795984747723973,
-    .00629918049732845,
-    .00462793522803742,
-    .00294910295364247,
-    .00126779163408536		#75 (indexed from 0)
-])
-
-Gauss76Z = np.array([
-    -.999505948362153,		#0
-    -.997397786355355,
-    -.993608772723527,
-    -.988144453359837,
-    -.981013938975656,
-    -.972229228520377,
-    -.961805126758768,
-    -.949759207710896,
-    -.936111781934811,
-    -.92088586125215,
-    -.904107119545567,		#10
-    -.885803849292083,
-    -.866006913771982,
-    -.844749694983342,
-    -.822068037328975,
-    -.7980001871612,
-    -.77258672828181,
-    -.74587051350361,
-    -.717896592387704,
-    -.688712135277641,
-    -.658366353758143,		#20
-    -.626910417672267,
-    -.594397368836793,
-    -.560882031601237,
-    -.526420920401243,
-    -.491072144462194,
-    -.454895309813726,
-    -.417951418780327,
-    -.380302767117504,
-    -.342012838966962,
-    -.303146199807908,		#30
-    -.263768387584994,
-    -.223945802196474,
-    -.183745593528914,
-    -.143235548227268,
-    -.102483975391227,
-    -.0615595913906112,
-    -.0205314039939986,
-    .0205314039939986,
-    .0615595913906112,
-    .102483975391227,			#40
-    .143235548227268,
-    .183745593528914,
-    .223945802196474,
-    .263768387584994,
-    .303146199807908,
-    .342012838966962,
-    .380302767117504,
-    .417951418780327,
-    .454895309813726,
-    .491072144462194,		#50
-    .526420920401243,
-    .560882031601237,
-    .594397368836793,
-    .626910417672267,
-    .658366353758143,
-    .688712135277641,
-    .717896592387704,
-    .74587051350361,
-    .77258672828181,
-    .7980001871612,	#60
-    .822068037328975,
-    .844749694983342,
-    .866006913771982,
-    .885803849292083,
-    .904107119545567,
-    .92088586125215,
-    .936111781934811,
-    .949759207710896,
-    .961805126758768,
-    .972229228520377,		#70
-    .981013938975656,
-    .988144453359837,
-    .993608772723527,
-    .997397786355355,
-    .999505948362153		#75
-])
-
-Gauss150Z = np.array([
-    -0.9998723404457334,
-    -0.9993274305065947,
-    -0.9983473449340834,
-    -0.9969322929775997,
-    -0.9950828645255290,
-    -0.9927998590434373,
-    -0.9900842691660192,
-    -0.9869372772712794,
-    -0.9833602541697529,
-    -0.9793547582425894,
-    -0.9749225346595943,
-    -0.9700655145738374,
-    -0.9647858142586956,
-    -0.9590857341746905,
-    -0.9529677579610971,
-    -0.9464345513503147,
-    -0.9394889610042837,
-    -0.9321340132728527,
-    -0.9243729128743136,
-    -0.9162090414984952,
-    -0.9076459563329236,
-    -0.8986873885126239,
-    -0.8893372414942055,
-    -0.8795995893549102,
-    -0.8694786750173527,
-    -0.8589789084007133,
-    -0.8481048644991847,
-    -0.8368612813885015,
-    -0.8252530581614230,
-    -0.8132852527930605,
-    -0.8009630799369827,
-    -0.7882919086530552,
-    -0.7752772600680049,
-    -0.7619248049697269,
-    -0.7482403613363824,
-    -0.7342298918013638,
-    -0.7198995010552305,
-    -0.7052554331857488,
-    -0.6903040689571928,
-    -0.6750519230300931,
-    -0.6595056411226444,
-    -0.6436719971150083,
-    -0.6275578900977726,
-    -0.6111703413658551,
-    -0.5945164913591590,
-    -0.5776035965513142,
-    -0.5604390262878617,
-    -0.5430302595752546,
-    -0.5253848818220803,
-    -0.5075105815339176,
-    -0.4894151469632753,
-    -0.4711064627160663,
-    -0.4525925063160997,
-    -0.4338813447290861,
-    -0.4149811308476706,
-    -0.3959000999390257,
-    -0.3766465660565522,
-    -0.3572289184172501,
-    -0.3376556177463400,
-    -0.3179351925907259,
-    -0.2980762356029071,
-    -0.2780873997969574,
-    -0.2579773947782034,
-    -0.2377549829482451,
-    -0.2174289756869712,
-    -0.1970082295132342,
-    -0.1765016422258567,
-    -0.1559181490266516,
-    -0.1352667186271445,
-    -0.1145563493406956,
-    -0.0937960651617229,
-    -0.0729949118337358,
-    -0.0521619529078925,
-    -0.0313062657937972,
-    -0.0104369378042598,
-    0.0104369378042598,
-    0.0313062657937972,
-    0.0521619529078925,
-    0.0729949118337358,
-    0.0937960651617229,
-    0.1145563493406956,
-    0.1352667186271445,
-    0.1559181490266516,
-    0.1765016422258567,
-    0.1970082295132342,
-    0.2174289756869712,
-    0.2377549829482451,
-    0.2579773947782034,
-    0.2780873997969574,
-    0.2980762356029071,
-    0.3179351925907259,
-    0.3376556177463400,
-    0.3572289184172501,
-    0.3766465660565522,
-    0.3959000999390257,
-    0.4149811308476706,
-    0.4338813447290861,
-    0.4525925063160997,
-    0.4711064627160663,
-    0.4894151469632753,
-    0.5075105815339176,
-    0.5253848818220803,
-    0.5430302595752546,
-    0.5604390262878617,
-    0.5776035965513142,
-    0.5945164913591590,
-    0.6111703413658551,
-    0.6275578900977726,
-    0.6436719971150083,
-    0.6595056411226444,
-    0.6750519230300931,
-    0.6903040689571928,
-    0.7052554331857488,
-    0.7198995010552305,
-    0.7342298918013638,
-    0.7482403613363824,
-    0.7619248049697269,
-    0.7752772600680049,
-    0.7882919086530552,
-    0.8009630799369827,
-    0.8132852527930605,
-    0.8252530581614230,
-    0.8368612813885015,
-    0.8481048644991847,
-    0.8589789084007133,
-    0.8694786750173527,
-    0.8795995893549102,
-    0.8893372414942055,
-    0.8986873885126239,
-    0.9076459563329236,
-    0.9162090414984952,
-    0.9243729128743136,
-    0.9321340132728527,
-    0.9394889610042837,
-    0.9464345513503147,
-    0.9529677579610971,
-    0.9590857341746905,
-    0.9647858142586956,
-    0.9700655145738374,
-    0.9749225346595943,
-    0.9793547582425894,
-    0.9833602541697529,
-    0.9869372772712794,
-    0.9900842691660192,
-    0.9927998590434373,
-    0.9950828645255290,
-    0.9969322929775997,
-    0.9983473449340834,
-    0.9993274305065947,
-    0.9998723404457334
-])
-
-Gauss150Wt = np.array([
-    0.0003276086705538,
-    0.0007624720924706,
-    0.0011976474864367,
-    0.0016323569986067,
-    0.0020663664924131,
-    0.0024994789888943,
-    0.0029315036836558,
-    0.0033622516236779,
-    0.0037915348363451,
-    0.0042191661429919,
-    0.0046449591497966,
-    0.0050687282939456,
-    0.0054902889094487,
-    0.0059094573005900,
-    0.0063260508184704,
-    0.0067398879387430,
-    0.0071507883396855,
-    0.0075585729801782,
-    0.0079630641773633,
-    0.0083640856838475,
-    0.0087614627643580,
-    0.0091550222717888,
-    0.0095445927225849,
-    0.0099300043714212,
-    0.0103110892851360,
-    0.0106876814158841,
-    0.0110596166734735,
-    0.0114267329968529,
-    0.0117888704247183,
-    0.0121458711652067,
-    0.0124975796646449,
-    0.0128438426753249,
-    0.0131845093222756,
-    0.0135194311690004,
-    0.0138484622795371,
-    0.0141714592928592,
-    0.0144882814685445,
-    0.0147987907597169,
-    0.0151028518701744,
-    0.0154003323133401,
-    0.0156911024699895,
-    0.0159750356447283,
-    0.0162520081211971,
-    0.0165218992159766,
-    0.0167845913311726,
-    0.0170399700056559,
-    0.0172879239649355,
-    0.0175283451696437,
-    0.0177611288626114,
-    0.0179861736145128,
-    0.0182033813680609,
-    0.0184126574807331,
-    0.0186139107660094,
-    0.0188070535331042,
-    0.0189920016251754,
-    0.0191686744559934,
-    0.0193369950450545,
-    0.0194968900511231,
-    0.0196482898041878,
-    0.0197911283358190,
-    0.0199253434079123,
-    0.0200508765398072,
-    0.0201676730337687,
-    0.0202756819988200,
-    0.0203748563729175,
-    0.0204651529434560,
-    0.0205465323660984,
-    0.0206189591819181,
-    0.0206824018328499,
-    0.0207368326754401,
-    0.0207822279928917,
-    0.0208185680053983,
-    0.0208458368787627,
-    0.0208640227312962,
-    0.0208731176389954,
-    0.0208731176389954,
-    0.0208640227312962,
-    0.0208458368787627,
-    0.0208185680053983,
-    0.0207822279928917,
-    0.0207368326754401,
-    0.0206824018328499,
-    0.0206189591819181,
-    0.0205465323660984,
-    0.0204651529434560,
-    0.0203748563729175,
-    0.0202756819988200,
-    0.0201676730337687,
-    0.0200508765398072,
-    0.0199253434079123,
-    0.0197911283358190,
-    0.0196482898041878,
-    0.0194968900511231,
-    0.0193369950450545,
-    0.0191686744559934,
-    0.0189920016251754,
-    0.0188070535331042,
-    0.0186139107660094,
-    0.0184126574807331,
-    0.0182033813680609,
-    0.0179861736145128,
-    0.0177611288626114,
-    0.0175283451696437,
-    0.0172879239649355,
-    0.0170399700056559,
-    0.0167845913311726,
-    0.0165218992159766,
-    0.0162520081211971,
-    0.0159750356447283,
-    0.0156911024699895,
-    0.0154003323133401,
-    0.0151028518701744,
-    0.0147987907597169,
-    0.0144882814685445,
-    0.0141714592928592,
-    0.0138484622795371,
-    0.0135194311690004,
-    0.0131845093222756,
-    0.0128438426753249,
-    0.0124975796646449,
-    0.0121458711652067,
-    0.0117888704247183,
-    0.0114267329968529,
-    0.0110596166734735,
-    0.0106876814158841,
-    0.0103110892851360,
-    0.0099300043714212,
-    0.0095445927225849,
-    0.0091550222717888,
-    0.0087614627643580,
-    0.0083640856838475,
-    0.0079630641773633,
-    0.0075585729801782,
-    0.0071507883396855,
-    0.0067398879387430,
-    0.0063260508184704,
-    0.0059094573005900,
-    0.0054902889094487,
-    0.0050687282939456,
-    0.0046449591497966,
-    0.0042191661429919,
-    0.0037915348363451,
-    0.0033622516236779,
-    0.0029315036836558,
-    0.0024994789888943,
-    0.0020663664924131,
-    0.0016323569986067,
-    0.0011976474864367,
-    0.0007624720924706,
-    0.0003276086705538
-])
+class Gauss:
+    def __init__(self, w, z):
+        self.n = len(w)
+        self.w = w
+        self.z = z
+
+gauss20 = Gauss(
+    w=np.array([
+        .0176140071391521,
+        .0406014298003869,
+        .0626720483341091,
+        .0832767415767047,
+        .10193011981724,
+        .118194531961518,
+        .131688638449177,
+        .142096109318382,
+        .149172986472604,
+        .152753387130726,
+        .152753387130726,
+        .149172986472604,
+        .142096109318382,
+        .131688638449177,
+        .118194531961518,
+        .10193011981724,
+        .0832767415767047,
+        .0626720483341091,
+        .0406014298003869,
+        .0176140071391521
+    ]),
+    z=np.array([
+        -.993128599185095,
+        -.963971927277914,
+        -.912234428251326,
+        -.839116971822219,
+        -.746331906460151,
+        -.636053680726515,
+        -.510867001950827,
+        -.37370608871542,
+        -.227785851141645,
+        -.076526521133497,
+        .0765265211334973,
+        .227785851141645,
+        .37370608871542,
+        .510867001950827,
+        .636053680726515,
+        .746331906460151,
+        .839116971822219,
+        .912234428251326,
+        .963971927277914,
+        .993128599185095
+    ])
+)
+
+gauss76 = Gauss(
+    w=np.array([
+        .00126779163408536,		#0
+        .00294910295364247,
+        .00462793522803742,
+        .00629918049732845,
+        .00795984747723973,
+        .00960710541471375,
+        .0112381685696677,
+        .0128502838475101,
+        .0144407317482767,
+        .0160068299122486,
+        .0175459372914742,		#10
+        .0190554584671906,
+        .020532847967908,
+        .0219756145344162,
+        .0233813253070112,
+        .0247476099206597,
+        .026072164497986,
+        .0273527555318275,
+        .028587223650054,
+        .029773487255905,
+        .0309095460374916,		#20
+        .0319934843404216,
+        .0330234743977917,
+        .0339977794120564,
+        .0349147564835508,
+        .0357728593807139,
+        .0365706411473296,
+        .0373067565423816,
+        .0379799643084053,
+        .0385891292645067,
+        .0391332242205184,		#30
+        .0396113317090621,
+        .0400226455325968,
+        .040366472122844,
+        .0406422317102947,
+        .0408494593018285,
+        .040987805464794,
+        .0410570369162294,
+        .0410570369162294,
+        .040987805464794,
+        .0408494593018285,		#40
+        .0406422317102947,
+        .040366472122844,
+        .0400226455325968,
+        .0396113317090621,
+        .0391332242205184,
+        .0385891292645067,
+        .0379799643084053,
+        .0373067565423816,
+        .0365706411473296,
+        .0357728593807139,		#50
+        .0349147564835508,
+        .0339977794120564,
+        .0330234743977917,
+        .0319934843404216,
+        .0309095460374916,
+        .029773487255905,
+        .028587223650054,
+        .0273527555318275,
+        .026072164497986,
+        .0247476099206597,		#60
+        .0233813253070112,
+        .0219756145344162,
+        .020532847967908,
+        .0190554584671906,
+        .0175459372914742,
+        .0160068299122486,
+        .0144407317482767,
+        .0128502838475101,
+        .0112381685696677,
+        .00960710541471375,		#70
+        .00795984747723973,
+        .00629918049732845,
+        .00462793522803742,
+        .00294910295364247,
+        .00126779163408536		#75 (indexed from 0)
+    ]),
+    z=np.array([
+        -.999505948362153,		#0
+        -.997397786355355,
+        -.993608772723527,
+        -.988144453359837,
+        -.981013938975656,
+        -.972229228520377,
+        -.961805126758768,
+        -.949759207710896,
+        -.936111781934811,
+        -.92088586125215,
+        -.904107119545567,		#10
+        -.885803849292083,
+        -.866006913771982,
+        -.844749694983342,
+        -.822068037328975,
+        -.7980001871612,
+        -.77258672828181,
+        -.74587051350361,
+        -.717896592387704,
+        -.688712135277641,
+        -.658366353758143,		#20
+        -.626910417672267,
+        -.594397368836793,
+        -.560882031601237,
+        -.526420920401243,
+        -.491072144462194,
+        -.454895309813726,
+        -.417951418780327,
+        -.380302767117504,
+        -.342012838966962,
+        -.303146199807908,		#30
+        -.263768387584994,
+        -.223945802196474,
+        -.183745593528914,
+        -.143235548227268,
+        -.102483975391227,
+        -.0615595913906112,
+        -.0205314039939986,
+        .0205314039939986,
+        .0615595913906112,
+        .102483975391227,			#40
+        .143235548227268,
+        .183745593528914,
+        .223945802196474,
+        .263768387584994,
+        .303146199807908,
+        .342012838966962,
+        .380302767117504,
+        .417951418780327,
+        .454895309813726,
+        .491072144462194,		#50
+        .526420920401243,
+        .560882031601237,
+        .594397368836793,
+        .626910417672267,
+        .658366353758143,
+        .688712135277641,
+        .717896592387704,
+        .74587051350361,
+        .77258672828181,
+        .7980001871612,	#60
+        .822068037328975,
+        .844749694983342,
+        .866006913771982,
+        .885803849292083,
+        .904107119545567,
+        .92088586125215,
+        .936111781934811,
+        .949759207710896,
+        .961805126758768,
+        .972229228520377,		#70
+        .981013938975656,
+        .988144453359837,
+        .993608772723527,
+        .997397786355355,
+        .999505948362153		#75
+    ])
+)
+
+gauss150 = Gauss(
+    z=np.array([
+        -0.9998723404457334,
+        -0.9993274305065947,
+        -0.9983473449340834,
+        -0.9969322929775997,
+        -0.9950828645255290,
+        -0.9927998590434373,
+        -0.9900842691660192,
+        -0.9869372772712794,
+        -0.9833602541697529,
+        -0.9793547582425894,
+        -0.9749225346595943,
+        -0.9700655145738374,
+        -0.9647858142586956,
+        -0.9590857341746905,
+        -0.9529677579610971,
+        -0.9464345513503147,
+        -0.9394889610042837,
+        -0.9321340132728527,
+        -0.9243729128743136,
+        -0.9162090414984952,
+        -0.9076459563329236,
+        -0.8986873885126239,
+        -0.8893372414942055,
+        -0.8795995893549102,
+        -0.8694786750173527,
+        -0.8589789084007133,
+        -0.8481048644991847,
+        -0.8368612813885015,
+        -0.8252530581614230,
+        -0.8132852527930605,
+        -0.8009630799369827,
+        -0.7882919086530552,
+        -0.7752772600680049,
+        -0.7619248049697269,
+        -0.7482403613363824,
+        -0.7342298918013638,
+        -0.7198995010552305,
+        -0.7052554331857488,
+        -0.6903040689571928,
+        -0.6750519230300931,
+        -0.6595056411226444,
+        -0.6436719971150083,
+        -0.6275578900977726,
+        -0.6111703413658551,
+        -0.5945164913591590,
+        -0.5776035965513142,
+        -0.5604390262878617,
+        -0.5430302595752546,
+        -0.5253848818220803,
+        -0.5075105815339176,
+        -0.4894151469632753,
+        -0.4711064627160663,
+        -0.4525925063160997,
+        -0.4338813447290861,
+        -0.4149811308476706,
+        -0.3959000999390257,
+        -0.3766465660565522,
+        -0.3572289184172501,
+        -0.3376556177463400,
+        -0.3179351925907259,
+        -0.2980762356029071,
+        -0.2780873997969574,
+        -0.2579773947782034,
+        -0.2377549829482451,
+        -0.2174289756869712,
+        -0.1970082295132342,
+        -0.1765016422258567,
+        -0.1559181490266516,
+        -0.1352667186271445,
+        -0.1145563493406956,
+        -0.0937960651617229,
+        -0.0729949118337358,
+        -0.0521619529078925,
+        -0.0313062657937972,
+        -0.0104369378042598,
+        0.0104369378042598,
+        0.0313062657937972,
+        0.0521619529078925,
+        0.0729949118337358,
+        0.0937960651617229,
+        0.1145563493406956,
+        0.1352667186271445,
+        0.1559181490266516,
+        0.1765016422258567,
+        0.1970082295132342,
+        0.2174289756869712,
+        0.2377549829482451,
+        0.2579773947782034,
+        0.2780873997969574,
+        0.2980762356029071,
+        0.3179351925907259,
+        0.3376556177463400,
+        0.3572289184172501,
+        0.3766465660565522,
+        0.3959000999390257,
+        0.4149811308476706,
+        0.4338813447290861,
+        0.4525925063160997,
+        0.4711064627160663,
+        0.4894151469632753,
+        0.5075105815339176,
+        0.5253848818220803,
+        0.5430302595752546,
+        0.5604390262878617,
+        0.5776035965513142,
+        0.5945164913591590,
+        0.6111703413658551,
+        0.6275578900977726,
+        0.6436719971150083,
+        0.6595056411226444,
+        0.6750519230300931,
+        0.6903040689571928,
+        0.7052554331857488,
+        0.7198995010552305,
+        0.7342298918013638,
+        0.7482403613363824,
+        0.7619248049697269,
+        0.7752772600680049,
+        0.7882919086530552,
+        0.8009630799369827,
+        0.8132852527930605,
+        0.8252530581614230,
+        0.8368612813885015,
+        0.8481048644991847,
+        0.8589789084007133,
+        0.8694786750173527,
+        0.8795995893549102,
+        0.8893372414942055,
+        0.8986873885126239,
+        0.9076459563329236,
+        0.9162090414984952,
+        0.9243729128743136,
+        0.9321340132728527,
+        0.9394889610042837,
+        0.9464345513503147,
+        0.9529677579610971,
+        0.9590857341746905,
+        0.9647858142586956,
+        0.9700655145738374,
+        0.9749225346595943,
+        0.9793547582425894,
+        0.9833602541697529,
+        0.9869372772712794,
+        0.9900842691660192,
+        0.9927998590434373,
+        0.9950828645255290,
+        0.9969322929775997,
+        0.9983473449340834,
+        0.9993274305065947,
+        0.9998723404457334
+    ]),
+    w=np.array([
+        0.0003276086705538,
+        0.0007624720924706,
+        0.0011976474864367,
+        0.0016323569986067,
+        0.0020663664924131,
+        0.0024994789888943,
+        0.0029315036836558,
+        0.0033622516236779,
+        0.0037915348363451,
+        0.0042191661429919,
+        0.0046449591497966,
+        0.0050687282939456,
+        0.0054902889094487,
+        0.0059094573005900,
+        0.0063260508184704,
+        0.0067398879387430,
+        0.0071507883396855,
+        0.0075585729801782,
+        0.0079630641773633,
+        0.0083640856838475,
+        0.0087614627643580,
+        0.0091550222717888,
+        0.0095445927225849,
+        0.0099300043714212,
+        0.0103110892851360,
+        0.0106876814158841,
+        0.0110596166734735,
+        0.0114267329968529,
+        0.0117888704247183,
+        0.0121458711652067,
+        0.0124975796646449,
+        0.0128438426753249,
+        0.0131845093222756,
+        0.0135194311690004,
+        0.0138484622795371,
+        0.0141714592928592,
+        0.0144882814685445,
+        0.0147987907597169,
+        0.0151028518701744,
+        0.0154003323133401,
+        0.0156911024699895,
+        0.0159750356447283,
+        0.0162520081211971,
+        0.0165218992159766,
+        0.0167845913311726,
+        0.0170399700056559,
+        0.0172879239649355,
+        0.0175283451696437,
+        0.0177611288626114,
+        0.0179861736145128,
+        0.0182033813680609,
+        0.0184126574807331,
+        0.0186139107660094,
+        0.0188070535331042,
+        0.0189920016251754,
+        0.0191686744559934,
+        0.0193369950450545,
+        0.0194968900511231,
+        0.0196482898041878,
+        0.0197911283358190,
+        0.0199253434079123,
+        0.0200508765398072,
+        0.0201676730337687,
+        0.0202756819988200,
+        0.0203748563729175,
+        0.0204651529434560,
+        0.0205465323660984,
+        0.0206189591819181,
+        0.0206824018328499,
+        0.0207368326754401,
+        0.0207822279928917,
+        0.0208185680053983,
+        0.0208458368787627,
+        0.0208640227312962,
+        0.0208731176389954,
+        0.0208731176389954,
+        0.0208640227312962,
+        0.0208458368787627,
+        0.0208185680053983,
+        0.0207822279928917,
+        0.0207368326754401,
+        0.0206824018328499,
+        0.0206189591819181,
+        0.0205465323660984,
+        0.0204651529434560,
+        0.0203748563729175,
+        0.0202756819988200,
+        0.0201676730337687,
+        0.0200508765398072,
+        0.0199253434079123,
+        0.0197911283358190,
+        0.0196482898041878,
+        0.0194968900511231,
+        0.0193369950450545,
+        0.0191686744559934,
+        0.0189920016251754,
+        0.0188070535331042,
+        0.0186139107660094,
+        0.0184126574807331,
+        0.0182033813680609,
+        0.0179861736145128,
+        0.0177611288626114,
+        0.0175283451696437,
+        0.0172879239649355,
+        0.0170399700056559,
+        0.0167845913311726,
+        0.0165218992159766,
+        0.0162520081211971,
+        0.0159750356447283,
+        0.0156911024699895,
+        0.0154003323133401,
+        0.0151028518701744,
+        0.0147987907597169,
+        0.0144882814685445,
+        0.0141714592928592,
+        0.0138484622795371,
+        0.0135194311690004,
+        0.0131845093222756,
+        0.0128438426753249,
+        0.0124975796646449,
+        0.0121458711652067,
+        0.0117888704247183,
+        0.0114267329968529,
+        0.0110596166734735,
+        0.0106876814158841,
+        0.0103110892851360,
+        0.0099300043714212,
+        0.0095445927225849,
+        0.0091550222717888,
+        0.0087614627643580,
+        0.0083640856838475,
+        0.0079630641773633,
+        0.0075585729801782,
+        0.0071507883396855,
+        0.0067398879387430,
+        0.0063260508184704,
+        0.0059094573005900,
+        0.0054902889094487,
+        0.0050687282939456,
+        0.0046449591497966,
+        0.0042191661429919,
+        0.0037915348363451,
+        0.0033622516236779,
+        0.0029315036836558,
+        0.0024994789888943,
+        0.0020663664924131,
+        0.0016323569986067,
+        0.0011976474864367,
+        0.0007624720924706,
+        0.0003276086705538
+    ])
+)
