Index: doc/Makefile
===================================================================
--- doc/Makefile	(revision 63776d3ac756a8c626dc0871fbe9eb1b062fc82f)
+++ doc/Makefile	(revision a84a0cac83cecece77ff644df4f2552ef1452b62)
Index: doc/rst_prolog
===================================================================
--- doc/rst_prolog	(revision e58bd7a11687d3a5a58879fd0bb4e09e59eba90f)
+++ doc/rst_prolog	(revision a0fb06a2ed023b879d041098faa161e70cffcd07)
@@ -1,4 +1,5 @@
 .. Set up some substitutions to make life easier...
 .. Remove |biggamma|, etc. when they are no longer needed.
+
 
 .. |alpha| unicode:: U+03B1
@@ -32,8 +33,11 @@
 .. |bigzeta| unicode:: U+039E
 .. |bigpsi| unicode:: U+03A8
+.. |bigphi| unicode:: U+03A6
+.. |bigsigma| unicode:: U+03A3
 .. |Gamma| unicode:: U+0393
 .. |Delta| unicode:: U+0394
 .. |Zeta| unicode:: U+039E
 .. |Psi| unicode:: U+03A8
+
 
 .. |drho| replace:: |Delta|\ |rho|
@@ -57,4 +61,9 @@
 
 
+.. |equiv| unicode:: U+2261
+.. |noteql| unicode:: U+2260
+.. |TM| unicode:: U+2122
+
+
 .. |cdot| unicode:: U+00B7
 .. |deg| unicode:: U+00B0
Index: example/fit_sesans.py
===================================================================
--- example/fit_sesans.py	(revision 52e3eef226a30fdeec221ab336eb9dc6918d833f)
+++ example/fit_sesans.py	(revision d7cd7d174169fe7f64ba68848446305ab7be5b3d)
@@ -14,4 +14,7 @@
 sys.path.insert(0, SASMODELS)
 
-from bumps.gui.gui_app import main as gui
-gui()
+if sys.argv[-1].startswith('-'):
+    from bumps.cli import main as run_bumps
+else:
+    from bumps.gui.gui_app import main as run_bumps
+run_bumps()
Index: sasmodels/bumps_model.py
===================================================================
--- sasmodels/bumps_model.py	(revision c5dadbbaf797cd12844168a3697683ea6cf1c964)
+++ sasmodels/bumps_model.py	(revision a84a0cac83cecece77ff644df4f2552ef1452b62)
Index: sasmodels/kernelpy.py
===================================================================
--- sasmodels/kernelpy.py	(revision 35647abd3c79d3ca6880d3b9913181dffce3a86f)
+++ sasmodels/kernelpy.py	(revision 35647abd3c79d3ca6880d3b9913181dffce3a86f)
@@ -264,2 +264,264 @@
     result = scale * ret / norm + background
     return result
+=======
+"""
+Python driver for python kernels
+
+Calls the kernel with a vector of $q$ values for a single parameter set.
+Polydispersity is supported by looping over different parameter sets and
+summing the results.  The interface to :class:`PyModel` matches those for
+:class:`kernelcl.GpuModel` and :class:`kerneldll.DllModel`.
+"""
+import numpy as np
+from numpy import pi, cos
+
+from .generate import F64
+
+class PyModel(object):
+    """
+    Wrapper for pure python models.
+    """
+    def __init__(self, model_info):
+        self.info = model_info
+
+    def __call__(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)
+
+    def release(self):
+        """
+        Free resources associated with the model.
+        """
+        pass
+
+class PyInput(object):
+    """
+    Make q data available to the gpu.
+
+    *q_vectors* is a list of q vectors, which will be *[q]* for 1-D data,
+    and *[qx, qy]* for 2-D data.  Internally, the vectors will be reallocated
+    to get the best performance on OpenCL, which may involve shifting and
+    stretching the array to better match the memory architecture.  Additional
+    points will be evaluated with *q=1e-3*.
+
+    *dtype* is the data type for the q vectors. The data type should be
+    set to match that of the kernel, which is an attribute of
+    :class:`GpuProgram`.  Note that not all kernels support double
+    precision, so even if the program was created for double precision,
+    the *GpuProgram.dtype* may be single precision.
+
+    Call :meth:`release` when complete.  Even if not called directly, the
+    buffer will be released when the data object is freed.
+    """
+    def __init__(self, q_vectors, dtype):
+        self.nq = q_vectors[0].size
+        self.dtype = dtype
+        self.is_2d = (len(q_vectors) == 2)
+        self.q_vectors = [np.ascontiguousarray(q, self.dtype) for q in q_vectors]
+        self.q_pointers = [q.ctypes.data for q in self.q_vectors]
+
+    def release(self):
+        """
+        Free resources associated with the model inputs.
+        """
+        self.q_vectors = []
+
+class PyKernel(object):
+    """
+    Callable SAS kernel.
+
+    *kernel* is the DllKernel object to call.
+
+    *model_info* is the module information
+
+    *q_input* is the DllInput q vectors at which the kernel should be
+    evaluated.
+
+    The resulting call method takes the *pars*, a list of values for
+    the fixed parameters to the kernel, and *pd_pars*, a list of (value,weight)
+    vectors for the polydisperse parameters.  *cutoff* determines the
+    integration limits: any points with combined weight less than *cutoff*
+    will not be calculated.
+
+    Call :meth:`release` when done with the kernel instance.
+    """
+    def __init__(self, kernel, model_info, q_input):
+        self.info = model_info
+        self.q_input = q_input
+        self.res = np.empty(q_input.nq, q_input.dtype)
+        dim = '2d' if q_input.is_2d else '1d'
+        # Loop over q unless user promises that the kernel is vectorized by
+        # taggining it with vectorized=True
+        if not getattr(kernel, 'vectorized', False):
+            if dim == '2d':
+                def vector_kernel(qx, qy, *args):
+                    """
+                    Vectorized 2D kernel.
+                    """
+                    return np.array([kernel(qxi, qyi, *args)
+                                     for qxi, qyi in zip(qx, qy)])
+            else:
+                def vector_kernel(q, *args):
+                    """
+                    Vectorized 1D kernel.
+                    """
+                    return np.array([kernel(qi, *args)
+                                     for qi in q])
+            self.kernel = vector_kernel
+        else:
+            self.kernel = kernel
+        fixed_pars = model_info['partype']['fixed-' + dim]
+        pd_pars = model_info['partype']['pd-' + dim]
+        vol_pars = model_info['partype']['volume']
+
+        # First two fixed pars are scale and background
+        pars = [p.name for p in model_info['parameters'][2:]]
+        offset = len(self.q_input.q_vectors)
+        self.args = self.q_input.q_vectors + [None] * len(pars)
+        self.fixed_index = np.array([pars.index(p) + offset
+                                     for p in fixed_pars[2:]])
+        self.pd_index = np.array([pars.index(p) + offset
+                                  for p in pd_pars])
+        self.vol_index = np.array([pars.index(p) + offset
+                                   for p in vol_pars])
+        try: self.theta_index = pars.index('theta') + offset
+        except ValueError: self.theta_index = -1
+
+        # Caller needs fixed_pars and pd_pars
+        self.fixed_pars = fixed_pars
+        self.pd_pars = pd_pars
+
+    def __call__(self, fixed, pd, cutoff=1e-5):
+        #print("fixed",fixed)
+        #print("pd", pd)
+        args = self.args[:]  # grab a copy of the args
+        form, form_volume = self.kernel, self.info['form_volume']
+        # First two fixed
+        scale, background = fixed[:2]
+        for index, value in zip(self.fixed_index, fixed[2:]):
+            args[index] = float(value)
+        res = _loops(form, form_volume, cutoff, scale, background, args,
+                     pd, self.pd_index, self.vol_index, self.theta_index)
+
+        return res
+
+    def release(self):
+        """
+        Free resources associated with the kernel.
+        """
+        self.q_input = None
+
+def _loops(form, form_volume, cutoff, scale, background,
+           args, pd, pd_index, vol_index, theta_index):
+    """
+    *form* is the name of the form function, which should be vectorized over
+    q, but otherwise have an interface like the opencl kernels, with the
+    q parameters first followed by the individual parameters in the order
+    given in model.parameters (see :mod:`sasmodels.generate`).
+
+    *form_volume* calculates the volume of the shape.  *vol_index* gives
+    the list of volume parameters
+
+    *cutoff* ignores the corners of the dispersion hypercube
+
+    *scale*, *background* multiplies the resulting form and adds an offset
+
+    *args* is the prepopulated set of arguments to the form function, starting
+    with the q vectors, and including slots for all the parameters.  The
+    values for the parameters will be substituted with values from the
+    dispersion functions.
+
+    *pd* is the list of dispersion parameters
+
+    *pd_index* are the indices of the dispersion parameters in *args*
+
+    *vol_index* are the indices of the volume parameters in *args*
+
+    *theta_index* is the index of the theta parameter for the sasview
+    spherical correction, or -1 if there is no angular dispersion
+    """
+
+    ################################################################
+    #                                                              #
+    #   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!   #
+    #   !!                                                    !!   #
+    #   !!  KEEP THIS CODE CONSISTENT WITH KERNEL_TEMPLATE.C  !!   #
+    #   !!                                                    !!   #
+    #   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!   #
+    #                                                              #
+    ################################################################
+
+    weight = np.empty(len(pd), 'd')
+    if weight.size > 0:
+        # weight vector, to be populated by polydispersity loops
+
+        # identify which pd parameters are volume parameters
+        vol_weight_index = np.array([(index in vol_index) for index in pd_index])
+
+        # Sort parameters in decreasing order of pd length
+        Npd = np.array([len(pdi[0]) for pdi in pd], 'i')
+        order = np.argsort(Npd)[::-1]
+        stride = np.cumprod(Npd[order])
+        pd = [pd[index] for index in order]
+        pd_index = pd_index[order]
+        vol_weight_index = vol_weight_index[order]
+
+        fast_value = pd[0][0]
+        fast_weight = pd[0][1]
+    else:
+        stride = np.array([1])
+        vol_weight_index = slice(None, None)
+        # keep lint happy
+        fast_value = [None]
+        fast_weight = [None]
+
+    ret = np.zeros_like(args[0])
+    norm = np.zeros_like(ret)
+    vol = np.zeros_like(ret)
+    vol_norm = np.zeros_like(ret)
+    for k in range(stride[-1]):
+        # update polydispersity parameter values
+        fast_index = k % stride[0]
+        if fast_index == 0:  # bottom loop complete ... check all other loops
+            if weight.size > 0:
+                for i, index, in enumerate(k % stride):
+                    args[pd_index[i]] = pd[i][0][index]
+                    weight[i] = pd[i][1][index]
+        else:
+            args[pd_index[0]] = fast_value[fast_index]
+            weight[0] = fast_weight[fast_index]
+
+        # Computes the weight, and if it is not sufficient then ignore this
+        # parameter set.
+        # Note: could precompute w1*...*wn so we only need to multiply by w0
+        w = np.prod(weight)
+        if w > cutoff:
+            # Note: can precompute spherical correction if theta_index is not
+            # the fast index. Correction factor for spherical integration
+            #spherical_correction = abs(cos(pi*args[phi_index])) if phi_index>=0 else 1.0
+            spherical_correction = (abs(cos(pi * args[theta_index])) * pi / 2
+                                    if theta_index >= 0 else 1.0)
+            #spherical_correction = 1.0
+
+            # Call the scattering function and adds it to the total.
+            I = form(*args)
+            if np.isnan(I).any(): continue
+            ret += w * I * spherical_correction
+            norm += w
+
+            # Volume normalization.
+            # If there are "volume" polydispersity parameters, then these
+            # will be used to call the form_volume function from the user
+            # supplied kernel, and accumulate a normalized weight.
+            # Note: can precompute volume norm if fast index is not a volume
+            if form_volume:
+                vol_args = [args[index] for index in vol_index]
+                vol_weight = np.prod(weight[vol_weight_index])
+                vol += vol_weight * form_volume(*vol_args)
+                vol_norm += vol_weight
+
+    positive = (vol * vol_norm != 0.0)
+    ret[positive] *= vol_norm[positive] / vol[positive]
+    result = scale * ret / norm + background
+    return result
Index: sasmodels/list_pars.py
===================================================================
--- sasmodels/list_pars.py	(revision fcd7bbda85a603b85101ba7ec6495729497e6b3f)
+++ sasmodels/list_pars.py	(revision a84a0cac83cecece77ff644df4f2552ef1452b62)
Index: sasmodels/model_test.py
===================================================================
--- sasmodels/model_test.py	(revision fcd7bbda85a603b85101ba7ec6495729497e6b3f)
+++ sasmodels/model_test.py	(revision a84a0cac83cecece77ff644df4f2552ef1452b62)
Index: sasmodels/models/be_polyelectrolyte.py
===================================================================
--- sasmodels/models/be_polyelectrolyte.py	(revision 6dd90c1d8aaff9b7299ee215555203db80eaa3a0)
+++ sasmodels/models/be_polyelectrolyte.py	(revision a84a0cac83cecece77ff644df4f2552ef1452b62)
Index: sasmodels/models/guinier.py
===================================================================
--- sasmodels/models/guinier.py	(revision 6dd90c1d8aaff9b7299ee215555203db80eaa3a0)
+++ sasmodels/models/guinier.py	(revision a84a0cac83cecece77ff644df4f2552ef1452b62)
Index: sasmodels/models/line.py
===================================================================
--- sasmodels/models/line.py	(revision 6dd90c1d8aaff9b7299ee215555203db80eaa3a0)
+++ sasmodels/models/line.py	(revision a84a0cac83cecece77ff644df4f2552ef1452b62)
Index: sasmodels/models/sc_crystal.py
===================================================================
--- sasmodels/models/sc_crystal.py	(revision 6dd90c1d8aaff9b7299ee215555203db80eaa3a0)
+++ sasmodels/models/sc_crystal.py	(revision a84a0cac83cecece77ff644df4f2552ef1452b62)
Index: sasmodels/models/squarewell.py
===================================================================
--- sasmodels/models/squarewell.py	(revision d529d93bd045e44913320de5e1c8db47f680e037)
+++ sasmodels/models/squarewell.py	(revision a84a0cac83cecece77ff644df4f2552ef1452b62)
