Index: sasmodels/convert.json
===================================================================
--- sasmodels/convert.json	(revision 8bd7b77da137d537b08a21d51f0a8000689d4cc6)
+++ sasmodels/convert.json	(revision 6e7ff6d026d856e1eb236a05975a2600ba9ba2c9)
@@ -625,4 +625,12 @@
     }
   ], 
+  "_spherepy": [
+    "SphereModel",
+    {
+      "sld": "sldSph",
+      "radius": "radius",
+      "sld_solvent": "sldSolv"
+    }
+  ],
   "spherical_sld": [
     "SphereSLDModel", 
Index: sasmodels/generate.py
===================================================================
--- sasmodels/generate.py	(revision e6408d049d3598925e345d8adec1cdb0bb0c6adb)
+++ sasmodels/generate.py	(revision 6e7ff6d026d856e1eb236a05975a2600ba9ba2c9)
@@ -125,25 +125,4 @@
 An *model_info* dictionary is constructed from the kernel meta data and
 returned to the caller.
-
-The model evaluator, function call sequence consists of q inputs and the return vector,
-followed by the loop value/weight vector, followed by the values for
-the non-polydisperse parameters, followed by the lengths of the
-polydispersity loops.  To construct the call for 1D models, the
-categories *fixed-1d* and *pd-1d* list the names of the parameters
-of the non-polydisperse and the polydisperse parameters respectively.
-Similarly, *fixed-2d* and *pd-2d* provide parameter names for 2D models.
-The *pd-rel* category is a set of those parameters which give
-polydispersitiy as a portion of the value (so a 10% length dispersity
-would use a polydispersity value of 0.1) rather than absolute
-dispersity such as an angle plus or minus 15 degrees.
-
-The *volume* category lists the volume parameters in order for calls
-to volume within the kernel (used for volume normalization) and for
-calls to ER and VR for effective radius and volume ratio respectively.
-
-The *orientation* and *magnetic* categories list the orientation and
-magnetic parameters.  These are used by the sasview interface.  The
-blank category is for parameters such as scale which don't have any
-other marking.
 
 The doc string at the start of the kernel module will be used to
@@ -552,30 +531,43 @@
     @property
     def ctypes(self): return self.details.ctypes
+
     @property
     def pd_par(self): return self._pd_par
+
     @property
     def pd_length(self): return self._pd_length
+
     @property
     def pd_offset(self): return self._pd_offset
+
     @property
     def pd_stride(self): return self._pd_stride
+
     @property
     def pd_coord(self): return self._pd_coord
+
     @property
     def par_coord(self): return self._par_coord
+
     @property
     def par_offset(self): return self._par_offset
+
+    @property
+    def num_active(self): return self.details[-4]
+    @num_active.setter
+    def num_active(self, v): self.details[-4] = v
+
+    @property
+    def total_pd(self): return self.details[-3]
+    @total_pd.setter
+    def total_pd(self, v): self.details[-3] = v
+
     @property
     def num_coord(self): return self.details[-2]
     @num_coord.setter
     def num_coord(self, v): self.details[-2] = v
-    @property
-    def total_pd(self): return self.details[-3]
-    @total_pd.setter
-    def total_pd(self, v): self.details[-3] = v
-    @property
-    def num_active(self): return self.details[-4]
-    @num_active.setter
-    def num_active(self, v): self.details[-4] = v
+
+    @property
+    def theta_par(self): return self.details[-1]
 
     def show(self):
@@ -638,4 +630,38 @@
 
 
+def create_vector_Iq(model_info):
+    """
+    Define Iq as a vector function if it exists.
+    """
+    Iq = model_info['Iq']
+    if callable(Iq) and not getattr(Iq, 'vectorized', False):
+        def vector_Iq(q, *args):
+            """
+            Vectorized 1D kernel.
+            """
+            return np.array([Iq(qi, *args) for qi in q])
+        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']
+    if callable(Iqxy) and not getattr(Iqxy, 'vectorized', False):
+        def vector_Iqxy(qx, qy, *args):
+            """
+            Vectorized 2D kernel.
+            """
+            return np.array([Iqxy(qxi, qyi, *args) for qxi, qyi in zip(qx, qy)])
+        model_info['Iqxy'] = vector_Iqxy
+    elif callable(Iq):
+        # Iq is vectorized because create_vector_Iq was already called.
+        def default_Iqxy(qx, qy, *args):
+            """
+            Default 2D kernel.
+            """
+            return Iq(np.sqrt(qx**2 + qy**2), *args)
+        model_info['Iqxy'] = default_Iqxy
+
 def create_default_functions(model_info):
     """
@@ -643,15 +669,9 @@
 
     This only works for Iqxy when Iq is written in python. :func:`make_source`
-    performs a similar role for Iq written in C.
-    """
-    if callable(model_info['Iq']) and model_info['Iqxy'] is None:
-        partable = model_info['parameters']
-        if partable.has_2d:
-            raise ValueError("Iqxy model is missing")
-        Iq = model_info['Iq']
-        def Iqxy(qx, qy, **kw):
-            return Iq(np.sqrt(qx**2 + qy**2), **kw)
-        model_info['Iqxy'] = Iqxy
-
+    performs a similar role for Iq written in C.  This also vectorizes
+    any functions that are not already marked as vectorized.
+    """
+    create_vector_Iq(model_info)
+    create_vector_Iqxy(model_info)  # call create_vector_Iq() first
 
 def load_kernel_module(model_name):
Index: sasmodels/kernel_iq.c
===================================================================
--- sasmodels/kernel_iq.c	(revision 398aa94342eed23fadbe04717b74da3d6e9470e4)
+++ sasmodels/kernel_iq.c	(revision 6e7ff6d026d856e1eb236a05975a2600ba9ba2c9)
@@ -42,5 +42,5 @@
     const int32_t pd_start,     // where we are in the polydispersity loop
     const int32_t pd_stop,      // where we are stopping in the polydispersity loop
-    global const ProblemDetails *problem,
+    global const ProblemDetails *details,
     global const double *weights,
     global const double *values,
@@ -60,5 +60,5 @@
   #endif
   for (int k=0; k < NPARS; k++) {
-    pvec[k] = values[problem->par_offset[k]];
+    pvec[k] = values[details->par_offset[k]];
   }
 
@@ -78,5 +78,5 @@
 
   // Monodisperse computation
-  if (problem->num_active == 0) {
+  if (details->num_active == 0) {
     #ifdef INVALID
     if (INVALID(local_values)) { return; }
@@ -116,5 +116,5 @@
   // Trigger the reset behaviour that happens at the end the fast loop
   // by setting the initial index >= weight vector length.
-  const int fast_length = problem->pd_length[0];
+  const int fast_length = details->pd_length[0];
   pd_index[0] = fast_length;
 
@@ -123,9 +123,9 @@
     // check if indices need to be updated
     if (pd_index[0] == fast_length) {
-      //printf("should be here with %d active\n", problem->num_active);
+      //printf("should be here with %d active\n", details->num_active);
 
       // Compute position in polydispersity hypercube
-      for (int k=0; k < problem->num_active; k++) {
-        pd_index[k] = (loop_index/problem->pd_stride[k])%problem->pd_length[k];
+      for (int k=0; k < details->num_active; k++) {
+        pd_index[k] = (loop_index/details->pd_stride[k])%details->pd_length[k];
         //printf("pd_index[%d] = %d\n",k,pd_index[k]);
       }
@@ -134,7 +134,7 @@
       partial_weight = 1.0;
       //printf("partial weight %d: ", loop_index);
-      for (int k=1; k < problem->num_active; k++) {
-        double wi = weights[problem->pd_offset[k] + pd_index[k]];
-        //printf("pd[%d]=par[%d]=%g ", k, problem->pd_par[k], wi);
+      for (int k=1; k < details->num_active; k++) {
+        double wi = weights[details->pd_offset[k] + pd_index[k]];
+        //printf("pd[%d]=par[%d]=%g ", k, details->pd_par[k], wi);
         partial_weight *= wi;
       }
@@ -143,13 +143,13 @@
       // Update parameter offsets in weight vector
       //printf("slow %d: ", loop_index);
-      for (int k=0; k < problem->num_coord; k++) {
-        int par = problem->par_coord[k];
-        int coord = problem->pd_coord[k];
-        int this_offset = problem->par_offset[par];
+      for (int k=0; k < details->num_coord; k++) {
+        int par = details->par_coord[k];
+        int coord = details->pd_coord[k];
+        int this_offset = details->par_offset[par];
         int block_size = 1;
         for (int bit=0; coord != 0; bit++) {
           if (coord&1) {
               this_offset += block_size * pd_index[bit];
-              block_size *= problem->pd_length[bit];
+              block_size *= details->pd_length[bit];
           }
           coord >>= 1;
@@ -159,6 +159,6 @@
         //printf("par[%d]=v[%d]=%g \n", k, offset[k], pvec[k]);
         // if theta is not coordinated with fast index, precompute spherical correction
-        if (par == problem->theta_par && !(problem->par_coord[k]&1)) {
-          spherical_correction = fmax(fabs(cos(M_PI_180*pvec[problem->theta_par])), 1e-6);
+        if (par == details->theta_par && !(details->par_coord[k]&1)) {
+          spherical_correction = fmax(fabs(cos(M_PI_180*pvec[details->theta_par])), 1e-6);
         }
       }
@@ -167,15 +167,15 @@
 
     // Increment fast index
-    const double wi = weights[problem->pd_offset[0] + pd_index[0]++];
+    const double wi = weights[details->pd_offset[0] + pd_index[0]++];
     double weight = partial_weight*wi;
     //printf("fast %d: ", loop_index);
-    for (int k=0; k < problem->num_coord; k++) {
-      if (problem->pd_coord[k]&1) {
-        const int par = problem->par_coord[k];
+    for (int k=0; k < details->num_coord; k++) {
+      if (details->pd_coord[k]&1) {
+        const int par = details->par_coord[k];
         pvec[par] = values[offset[par]++];
         //printf("p[%d]=v[%d]=%g ", par, offset[par]-1, pvec[par]);
         // if theta is coordinated with fast index, compute spherical correction each time
-        if (par == problem->theta_par) {
-          spherical_correction = fmax(fabs(cos(M_PI_180*pvec[problem->theta_par])), 1e-6);
+        if (par == details->theta_par) {
+          spherical_correction = fmax(fabs(cos(M_PI_180*pvec[details->theta_par])), 1e-6);
         }
       }
@@ -209,5 +209,5 @@
 
   // End of the PD loop we can normalize
-  if (pd_stop >= problem->total_pd) {
+  if (pd_stop >= details->total_pd) {
     const double scale = values[0];
     const double background = values[1];
@@ -216,5 +216,5 @@
     #endif
     for (int i=0; i < nq; i++) {
-      result[i] = (norm>0. ? scale*scattering/norm + background : background);
+      result[i] = (norm>0. ? scale*result[i]/norm + background : background);
     }
   }
Index: sasmodels/kernelpy.py
===================================================================
--- sasmodels/kernelpy.py	(revision 4d767110d389aa65268b4b929ac8df1413460380)
+++ sasmodels/kernelpy.py	(revision 6e7ff6d026d856e1eb236a05975a2600ba9ba2c9)
@@ -87,61 +87,63 @@
     """
     def __init__(self, kernel, model_info, q_input):
+        self.dtype = np.dtype('d')
         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)])
+        self.kernel = kernel
+        self.dim = '2d' if q_input.is_2d else '1d'
+
+        partable = model_info['parameters']
+        kernel_parameters = (partable.iqxy_parameters if q_input.is_2d
+                             else partable.iq_parameters)
+        volume_parameters = partable.form_volume_parameters
+
+        # Create an array to hold the parameter values.  There will be a
+        # single array whose values are updated as the calculator goes
+        # through the loop.  Arguments to the kernel and volume functions
+        # will use views into this vector, relying on the fact that a
+        # an array of no dimensions acts like a scalar.
+        parameter_vector = np.empty(len(partable.call_parameters), 'd')
+
+        # Create views into the array to hold the arguments
+        offset = 2
+        kernel_args, volume_args = [], []
+        for p in partable.kernel_parameters:
+            if p.length == 1:
+                # Scalar values are length 1 vectors with no dimensions.
+                v = parameter_vector[offset:offset+1].reshape(())
             else:
-                def vector_kernel(q, *args):
-                    """
-                    Vectorized 1D kernel.
-                    """
-                    return np.array([kernel(qi, *args)
-                                     for qi in q])
-            self.kernel = vector_kernel
+                # Vector values are simple views.
+                v = parameter_vector[offset:offset+p.length]
+            offset += p.length
+            if p in kernel_parameters:
+                kernel_args.append(v)
+            if p in volume_parameters:
+                volume_args.append(p)
+
+        # Hold on to the parameter vector so we can use it to call kernel later.
+        # This may also be required to preserve the views into the vector.
+        self._parameter_vector = parameter_vector
+
+        # Generate a closure which calls the kernel with the views into the
+        # parameter array.
+        if q_input.is_2d:
+            form = model_info['Iqxy']
+            qx, qy = q_input.q[:,0], q_input.q[:,1]
+            self._form = lambda: form(qx, qy, *kernel_args)
         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)
-
+            form = model_info['Iq']
+            q = q_input.q
+            self._form = lambda: form(q, *kernel_args)
+
+        # 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))
+
+    def __call__(self, details, weights, values, cutoff):
+        # type: (.generate.CoordinationDetails, np.ndarray, np.ndarray, float) -> np.ndarray
+        res = _loops(self._parameter_vector, self._form, self._volume,
+                     self.q_input.nq, details, weights, values, cutoff)
         return res
 
@@ -152,34 +154,13 @@
         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
-    """
-
+def _loops(parameters,  # type: np.ndarray
+           form,        # type: Callable[[], np.ndarray]
+           form_volume, # type: Callable[[], float]
+           nq,          # type: int
+           details,     # type: .generate.CoordinationDetails
+           weights,     # type: np.ndarray
+           values,      # type: np.ndarray
+           cutoff,      # type: float
+           ):           # type: (...) -> None
     ################################################################
     #                                                              #
@@ -191,76 +172,70 @@
     #                                                              #
     ################################################################
-
-    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]
+    parameters[2:] = values[details.par_offset]
+    scale, background = values[0], values[1]
+    if details.num_active == 0:
+        norm = float(form_volume())
+        if norm > 0.0:
+            return (scale/norm)*form() + background
+        else:
+            return np.ones(nq, 'd')*background
+
+    partial_weight = np.NaN
+    spherical_correction = 1.0
+    pd_stride = details.pd_stride[:details.num_active]
+    pd_length = details.pd_length[:details.num_active]
+    pd_offset = details.pd_offset[:details.num_active]
+    pd_index = np.empty_like(pd_offset)
+    offset = np.empty_like(details.par_offset)
+    theta = details.theta_par
+    fast_length = pd_length[0]
+    pd_index[0] = fast_length
+    total = np.zeros(nq, 'd')
+    norm = 0.0
+    for loop_index in range(details.total_pd):
+        # update polydispersity parameter values
+        if pd_index[0] == fast_length:
+            pd_index[:] = (loop_index/pd_stride)%pd_length
+            partial_weight = np.prod(weights[pd_offset+pd_index][1:])
+            for k in range(details.num_coord):
+                par = details.par_coord[k]
+                coord = details.pd_coord[k]
+                this_offset = details.par_offset[par]
+                block_size = 1
+                for bit in xrange(32):
+                    if coord&1:
+                        this_offset += block_size * pd_index[bit]
+                        block_size *= pd_length[bit]
+                    coord >>= 1
+                    if coord == 0: break
+                offset[par] = this_offset
+                parameters[par] = values[this_offset]
+                if par == theta and not (details.par_coord[k]&1):
+                    spherical_correction = max(abs(cos(pi/180 * parameters[theta])), 1e-6)
+        for k in range(details.num_coord):
+            if details.pd_coord[k]&1:
+                par = details.par_coord[k]
+                parameters[par] = values[offset[par]]
+                offset[par] += 1
+                if par == theta:
+                    spherical_correction = max(abs(cos(pi/180 * parameters[theta])), 1e-6)
+
+        weight = partial_weight * weights[pd_offset[0] + pd_index[0]]
+        pd_index[0] += 1
+        if weight > cutoff:
+            # Call the scattering function
+            # Assume that NaNs are only generated if the parameters are bad;
+            # exclude all q for that NaN.  Even better would be to have an
+            # INVALID expression like the C models, but that is too expensive.
+            I = form()
+            if np.isnan(I).any(): continue
+
+            # update value and norm
+            weight *= spherical_correction
+            total += weight * I
+            norm += weight * form_volume()
+
+    if norm > 0.0:
+        return (scale/norm)*total + background
     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
+        return np.ones(nq, 'd')*background
Index: sasmodels/model_test.py
===================================================================
--- sasmodels/model_test.py	(revision d2bb6049fb1ca2c1d60bcd3853b1d2ae889866b2)
+++ sasmodels/model_test.py	(revision 6e7ff6d026d856e1eb236a05975a2600ba9ba2c9)
@@ -89,5 +89,4 @@
 
         if is_py:  # kernel implemented in python
-            continue # TODO: re-enable python tests
             test_name = "Model: %s, Kernel: python"%model_name
             test_method_name = "test_%s_python" % model_name
Index: sasmodels/modelinfo.py
===================================================================
--- sasmodels/modelinfo.py	(revision ee8f73486ed89c512902019a13b040e576091f49)
+++ sasmodels/modelinfo.py	(revision 6e7ff6d026d856e1eb236a05975a2600ba9ba2c9)
@@ -189,5 +189,8 @@
     can automatically be promoted to magnetic parameters, each of which
     will have a magnitude and a direction, which may be different from
-    other sld parameters.
+    other sld parameters. The volume parameters are used for calls
+    to form_volume within the kernel (required for volume normalization)
+    and for calls to ER and VR for effective radius and volume ratio
+    respectively.
 
     *description* is a short description of the parameter.  This will
@@ -197,9 +200,16 @@
     Additional values can be set after the parameter is created:
 
-    *length* is the length of the field if it is a vector field
-    *length_control* is the parameter which sets the vector length
-    *is_control* is True if the parameter is a control parameter for a vector
-    *polydisperse* is true if the parameter accepts a polydispersity
-    *relative_pd* is true if that polydispersity is relative
+    * *length* is the length of the field if it is a vector field
+
+    * *length_control* is the parameter which sets the vector length
+
+    * *is_control* is True if the parameter is a control parameter for a vector
+
+    * *polydisperse* is true if the parameter accepts a polydispersity
+
+    * *relative_pd* is true if that polydispersity is a portion of the
+    value (so a 10% length dipsersity would use a polydispersity value of 0.1)
+    rather than absolute dispersisity (such as an angle plus or minus
+    15 degrees).
 
     In the usual process these values are set by :func:`make_parameter_table`
