Index: example/weights/cyclic_gaussian.py
===================================================================
--- example/weights/cyclic_gaussian.py	(revision 35d230086190fc31926c237440d996e7ce2c269b)
+++ example/weights/cyclic_gaussian.py	(revision 35d230086190fc31926c237440d996e7ce2c269b)
@@ -0,0 +1,39 @@
+import numpy as np
+from numpy import exp, sin, cos, pi, radians, degrees
+
+from sasmodels.weights import Dispersion as BaseDispersion
+
+class Dispersion(BaseDispersion):
+    r"""
+    Cyclic gaussian dispersion on orientation.
+
+    .. math:
+
+        w(\theta) = e^{-\frac{\sin^2 \theta}{2 \sigma^2}}
+
+    This provides a close match to the gaussian distribution for
+    low angles, but the tails are limited to $\pm 90^\circ$.  For $\sigma$
+    large the distribution is approximately uniform.  The usual polar coordinate
+    projection applies, with $\theta$ weights scaled by $\cos \theta$
+    and $\phi$ weights unscaled.
+
+    This is eqivalent to a Maier-Saupe distribution with order
+    parameter $a = 1/(2 \sigma^2)$, with $\sigma$ in radians.
+    """
+    type = "cyclic_gaussian"
+    default = dict(npts=35, width=1, nsigmas=3)
+
+    # Note: center is always zero for orientation distributions
+    def _weights(self, center, sigma, lb, ub):
+        # Convert sigma in degrees to radians
+        sigma = radians(sigma)
+
+        # Limit width to +/- 90 degrees
+        width = min(self.nsigmas*sigma, pi/2)
+        x = np.linspace(-width, width, self.npts)
+
+        # Truncate the distribution in case the parameter value is limited
+        x[(x >= radians(lb)) & (x <= radians(ub))]
+
+        # Return orientation in degrees with Maier-Saupe weights
+        return degrees(x), exp(-0.5*sin(x)**2/sigma**2)
Index: example/weights/gaussian_eq.py
===================================================================
--- example/weights/gaussian_eq.py	(revision 55e82f04d46e375f4f5aa858d1291b28da4d6219)
+++ example/weights/gaussian_eq.py	(revision 55e82f04d46e375f4f5aa858d1291b28da4d6219)
@@ -0,0 +1,42 @@
+import numpy as np
+import scipy.stats
+
+from sasmodels.weights import Dispersion as BaseDispersion
+
+class Dispersion(BaseDispersion):
+    r"""
+    Gaussian dispersion, with 1-$\sigma$ width.
+
+    .. math::
+
+        w = \exp\left(-\tfrac12 (x - c)^2/\sigma^2\right)
+
+    $x$ points are chosen such that each interval has equal weight.
+
+    This works surprisingly poorly.  Try::
+
+        $ sascomp cylinder -2d theta=45 phi=20 phi_pd_type=gaussian_eq \
+          phi_pd_n=100,1000 radius=50 length=2*radius -midq phi_pd=5
+
+    Leaving it here for others to improve.
+    """
+    type = "gaussian_eq"
+    default = dict(npts=35, width=0, nsigmas=3)
+
+    def _weights(self, center, sigma, lb, ub):
+        # Use the gaussian distribution from scipy.stats
+        dist = scipy.stats.norm(center, sigma)
+
+        # Find the mid-points of the cdf intervals
+        cdf = np.linspace(0, 1, self.npts+2)[1:-1]
+        x = dist.ppf(cdf)
+
+        # Since we are equally spaced in cdf, all weights are the same
+        wx = np.ones_like(x)
+
+        # Truncate the distribution in case the parameter value is limited
+        index = (x >= lb) & (x <= ub)
+        x, wx = x[index], wx[index]
+
+        return x, wx
+
Index: example/weights/laplace.py
===================================================================
--- example/weights/laplace.py	(revision 342b3dd82d82df44b7021cc1cd6ee52766afdf36)
+++ example/weights/laplace.py	(revision 342b3dd82d82df44b7021cc1cd6ee52766afdf36)
@@ -0,0 +1,20 @@
+import numpy as np
+from scipy.stats import laplace
+
+from sasmodels import weights
+
+class Dispersion(weights.Dispersion):
+    r"""
+    Laplace distribution
+
+    .. math::
+
+        w(x) = e^{-\sigma |x - \mu|}
+    """
+    type = "laplace"
+    default = dict(npts=35, width=0, nsigmas=3)  # default values
+    def _weights(self, center, sigma, lb, ub):
+        x = self._linspace(center, sigma, lb, ub)
+        wx = laplace.pdf(x, center, sigma)
+        return x, wx
+
Index: example/weights/maier_saupe.py
===================================================================
--- example/weights/maier_saupe.py	(revision a5516b1b51923e98e6773ae919d62f40f1c6e567)
+++ example/weights/maier_saupe.py	(revision a5516b1b51923e98e6773ae919d62f40f1c6e567)
@@ -0,0 +1,132 @@
+from __future__ import print_function
+import numpy as np
+from numpy import exp, sin, degrees, radians, pi, sqrt
+
+from sasmodels.weights import Dispersion as BaseDispersion
+
+class Dispersion(BaseDispersion):
+    r"""
+    Maier-Saupe dispersion on orientation.
+
+    .. math:
+
+        w(\theta) = e^{a{\cos^2 \theta}}
+
+    This provides a close match to the gaussian distribution for
+    low angles, but the tails are limited to $\pm 90^\circ$.  For $a \ll 1$
+    the distribution is approximately uniform.  The usual polar coordinate
+    projection applies, with $\theta$ weights scaled by $\cos \theta$
+    and $\phi$ weights unscaled.
+
+    This is equivalent to a cyclic gaussian distribution
+    $w(\theta) = e^{-sin^2(\theta)/(2\a^2)}.
+
+    Note that the incorrect distribution is used for $a=0$.  With the
+    distribution parameter labelled as *width*, the value *width=0* is
+    is assumed to be completely oriented and no polydispersity distribution
+    is generated.  However a value of $a=0$ should be completely unoriented.
+    Any small value (e.g., 0.01 or lower) should suffice.
+
+    The order parameter $P_2$ is defined as
+
+    .. math:
+
+        P(a, \beta) = \frac{e^{a \cos^2 \beta}}{
+                       4\pi \int_0^{\pi/2} e^{a \cos^2 \beta} \sin \beta\,d\beta}
+
+        P_2 = 4\pi \int_0^{\pi/2} \frac{3 \cos^2 \beta - 1)}{2}
+                        P(a, \beta) \sin \beta\,d\beta
+
+    where $a$ is the distribution width $\sigma$ handed to the weights function.
+    There is a somewhat complicated closed form solution
+
+    .. math:
+
+        P_2 = \frac{3e^a}{2\sqrt{\pi a} E_a} - \frac{3}{4a} - \frac{1}{2}
+
+    where $E_a = \mathrm{erfi}(\sqrt{a})$ is the imaginary error function,
+    which can be coded in python as::
+
+        from numpy import pi, sqrt, exp
+        from scipy.special import erfi
+
+        def P_2(a):
+            # protect against overflow with a Taylor series at infinity
+            if a <= 700:
+                r = exp(a)/sqrt(pi*a)/erfi(sqrt(a))
+            else:
+                r = 1/((((6.5525/a + 1.875)/a + 0.75)/a + 0.5)/a + 1)
+            return 1.5*r - 0.75/a - 0.5
+
+    Given an order parameter $S = P_2(a)$, one can also solve for the
+    equivalent $a$:
+
+        from scipy.optimize import fsolve
+
+        def P_2_inv(S):
+            return fsolve(lambda x: P_2(x) - S, 1.0)[0]
+
+    References
+    ----------
+
+    [1] Hardouin, et al., 1995. SANS study of a semiflexible main chain
+    liquid crystalline polyether. *Macromolecules* 28, 5427-5433.
+    """
+    type = "maier_saupe"
+    default = dict(npts=35, width=1, nsigmas=None)
+
+    # Note: center is always zero for orientation distributions
+    def _weights(self, center, sigma, lb, ub):
+        # use the width parameter as the value for Maier-Saupe "a"
+        # and find the equivalent width sigma
+        a = sigma
+        sigma = 1./sqrt(2.*a)
+
+        # Limit width to +/- 90 degrees
+        width = min(self.nsigmas*sigma, pi/2)
+        x = np.linspace(-width, width, self.npts)
+
+        # Truncate the distribution in case the parameter value is limited
+        x[(x >= radians(lb)) & (x <= radians(ub))]
+
+        # Return orientation in degrees with Maier-Saupe weights
+        # Note: weights are normalized to sum to 1, so we can scale
+        # by an arbitrary scale factor c = exp(m) to get:
+        #     w = exp(m*cos(x)**2)/c = exp(-m*sin(x)**2)
+        return degrees(x), exp(-a*sin(x)**2)
+
+
+
+def P_2(a):
+    from numpy import pi, sqrt, exp
+    from scipy.special import erfi
+
+    # Double precision e^x overflows so do a Taylor expansion around infinity
+    # for large values of a.  With five terms it is accurate to 12 digits
+    # at a = 700, and 7 digits at a = 75.
+    if a <= 700:
+        r = exp(a)/sqrt(pi*a)/erfi(sqrt(a))
+    else:
+        r = 1/((((6.5525/a + 1.875)/a + 0.75)/a + 0.5)/a + 1)
+    return 1.5*r - 0.75/a - 0.5
+
+def P_2_inv(S):
+    from scipy.optimize import fsolve
+    return fsolve(lambda x: P_2(x) - S, 1.0)[0]
+
+def P_2_numerical(a):
+    from scipy.integrate import romberg
+    from numpy import cos, sin, pi, exp
+    def num(beta):
+        return (1.5 * cos(beta)**2 - 0.5) * exp(a * cos(beta)**2) * sin(beta)
+    def denom(beta):
+        return exp(a * cos(beta)**2) * sin(beta)
+    return romberg(num, 0, pi/2) / romberg(denom, 0, pi/2)
+
+if __name__ == "__main__":
+    import sys
+    a = float(sys.argv[1])
+    sigma = 1/(2*radians(a)**2)
+    #print("P_2", P_2(a), "difference from integral", P_2(a) - P_2_numerical(a))
+    print("a=%g, sigma=%g, P_2=%g, P_2_inv(P_2(a))-a=%g"
+          % (a, sigma, P_2(a), P_2_inv(P_2(a))-a))
Index: example/weights/maier_saupe_eq.py
===================================================================
--- example/weights/maier_saupe_eq.py	(revision 35d230086190fc31926c237440d996e7ce2c269b)
+++ example/weights/maier_saupe_eq.py	(revision 35d230086190fc31926c237440d996e7ce2c269b)
@@ -0,0 +1,63 @@
+import numpy as np
+from numpy import exp, sin, degrees, radians, pi, sqrt
+
+from sasmodels.weights import Dispersion as BaseDispersion
+
+class Dispersion(BaseDispersion):
+    r"""
+    Maier-Saupe dispersion on orientation (equal weights).
+
+    .. math:
+
+        w(\theta) = e^{a{\cos^2 \theta}}
+
+    This provides a close match to the gaussian distribution for
+    low angles, but the tails are limited to $\pm 90^\circ$.  For $a \ll 1$
+    the distribution is approximately uniform.  The usual polar coordinate
+    projection applies, with $\theta$ weights scaled by $\cos \theta$
+    and $\phi$ weights unscaled.
+
+    This is equivalent to a cyclic gaussian distribution
+    $w(\theta) = e^{-sin^2(\theta)/(2\sigma^2)}.
+
+    The $\theta$ points are spaced such that each interval has an
+    equal contribution to the distribution.
+
+    This works surprisingly poorly.  Try::
+
+        $ sascomp cylinder -2d theta=45 phi=20 phi_pd_type=maier_saupe_eq \
+          phi_pd_n=100,1000 radius=50 length=2*radius -midq phi_pd=5
+
+    Leaving it here for others to improve.
+    """
+    type = "maier_saupe_eq"
+    default = dict(npts=35, width=1, nsigmas=None)
+
+    # Note: center is always zero for orientation distributions
+    def _weights(self, center, sigma, lb, ub):
+        # use the width parameter as the value for Maier-Saupe "a"
+        a = sigma
+        sigma = 1./sqrt(2.*a)
+
+        # Create a lookup table for finding n points equally spaced
+        # in the cumulative density function.
+        # Limit width to +/-90 degrees.
+        width = min(self.nsigmas*sigma, pi/2)
+        xp = np.linspace(-width, width, max(self.npts*10, 100))
+
+        # Compute CDF. Since we normalized the sum of the weights to 1,
+        # we can scale by an arbitrary scale factor c = exp(m) to get:
+        #     w = exp(m*cos(x)**2)/c = exp(-m*sin(x)**2)
+        yp = np.cumsum(exp(-a*sin(xp)**2))
+        yp /= yp[-1]
+
+        # Find the mid-points of the equal-weighted intervals in the CDF
+        y = np.linspace(0, 1, self.npts+2)[1:-1]
+        x = np.interp(y, yp, xp)
+        wx = np.ones(self.npts)
+
+        # Truncate the distribution in case the parameter value is limited
+        index = (x >= lb) & (x <= ub)
+        x, wx = x[index], wx[index]
+
+        return degrees(x), wx
