Index: example/polynomial.py
===================================================================
--- example/polynomial.py	(revision c4993310e308c435dcf85328e919adef0ebc3ea1)
+++ example/polynomial.py	(revision d19962c0471a0eee538ce03020b5b226ba2752f6)
@@ -1,12 +1,12 @@
 from numpy import inf
 parameters = [
-    ["n", "", 0, [0,5], "", "polynomial degree"],
+    ["n", "", 1, [1,5], "", "number of coefficients (or degree+1)"],
     ["c[n]", "", 0, [-inf, inf], "", "coefficients to c_n x^n"],
 ]
 
-Iq = """
+Iq = r"""
     int int_n = (int)n;
-    double result = c[int_n];
-    for (int k=int_n-1; k >= 0; k--) { result = result*q + c[k]; }
+    double result = c[int_n-1];
+    for (int k=int_n-2; k >= 0; k--) { result = result*q + c[k]; }
     return result;
     """
Index: sasmodels/bumps_model.py
===================================================================
--- sasmodels/bumps_model.py	(revision 9217ef8174f5566df0d9259319dc1bde48372242)
+++ sasmodels/bumps_model.py	(revision d19962c0471a0eee538ce03020b5b226ba2752f6)
@@ -81,23 +81,19 @@
     from bumps.names import Parameter
 
-    pars = {} # => floating point parameters
+    pars = {}     # floating point parameters
+    pd_types = {} # distribution names
     for p in model_info['parameters']:
         value = kwargs.pop(p.name, p.default)
         pars[p.name] = Parameter.default(value, name=p.name, limits=p.limits)
-    for name in model_info['par_type']['pd']:
-        for xpart, xdefault, xlimits in [
-                ('_pd', 0., pars[name].limits),
-                ('_pd_n', 35., (0, 1000)),
-                ('_pd_nsigma', 3., (0, 10)),
-            ]:
-            xname = name + xpart
-            xvalue = kwargs.pop(xname, xdefault)
-            pars[xname] = Parameter.default(xvalue, name=xname, limits=xlimits)
-
-    pd_types = {}  # => distribution names
-    for name in model_info['par_type']['pd']:
-        xname = name + '_pd_type'
-        xvalue = kwargs.pop(xname, 'gaussian')
-        pd_types[xname] = xvalue
+        if p.polydisperse:
+            for part, default, limits in [
+                    ('_pd', 0., pars[p.name].limits),
+                    ('_pd_n', 35., (0, 1000)),
+                    ('_pd_nsigma', 3., (0, 10)),
+                ]:
+                name = p.name + part
+                value = kwargs.pop(name, default)
+                pars[name] = Parameter.default(value, name=name, limits=limits)
+            pd_types[p.name+'_pd_type'] = kwargs.pop(name, 'gaussian')
 
     if kwargs:  # args not corresponding to parameters
Index: sasmodels/compare.py
===================================================================
--- sasmodels/compare.py	(revision c4993310e308c435dcf85328e919adef0ebc3ea1)
+++ sasmodels/compare.py	(revision d19962c0471a0eee538ce03020b5b226ba2752f6)
@@ -308,23 +308,12 @@
     lines = []
     parameters = model_info['parameters']
-    for p in parameters.type['2d' if is2d else '1d']:
-        if p.length > 1:
-            for k in range(p.length):
-                ext = "[%d]"%k
-                fields = dict(
-                    value=pars.get(p.id+ext, p.default),
-                    pd=pars.get(p.id+"_pd"+ext, 0.),
-                    n=int(pars.get(p.id+"_pd_n"+ext, 0)),
-                    nsigma=pars.get(p.id+"_pd_nsgima"+ext, 3.),
-                    type=pars.get(p.id+"_pd_type"+ext, 'gaussian'))
-                lines.append(_format_par(p.id+ext, **fields))
-        else:
-            fields = dict(
-                value=pars.get(p.id, p.default),
-                pd=pars.get(p.id+"_pd", 0.),
-                n=int(pars.get(p.id+"_pd_n", 0)),
-                nsigma=pars.get(p.id+"_pd_nsgima", 3.),
-                type=pars.get(p.id+"_pd_type", 'gaussian'))
-            lines.append(_format_par(p.name, **fields))
+    for p in parameters.user_parameters(pars, is2d):
+        fields = dict(
+            value=pars.get(p.id, p.default),
+            pd=pars.get(p.id+"_pd", 0.),
+            n=int(pars.get(p.id+"_pd_n", 0)),
+            nsigma=pars.get(p.id+"_pd_nsgima", 3.),
+            type=pars.get(p.id+"_pd_type", 'gaussian'))
+        lines.append(_format_par(p.name, **fields))
     return "\n".join(lines)
 
@@ -668,5 +657,5 @@
     # Get the default values for the parameters
     pars = {}
-    for p in model_info['parameters']:
+    for p in model_info['parameters'].call_parameters:
         parts = [('', p.default)]
         if p.polydisperse:
@@ -677,5 +666,5 @@
         for ext, val in parts:
             if p.length > 1:
-                dict(("%s%s[%d]"%(p.id,ext,k), val) for k in range(p.length))
+                dict(("%s%d%s"%(p.id,k,ext), val) for k in range(p.length))
             else:
                 pars[p.id+ext] = val
@@ -709,8 +698,5 @@
     name = args[0]
     try:
-        if name.endswith('.py'):
-            model_info = core.load_model_info_from_path(name)
-        else:
-            model_info = core.load_model_info(name)
+        model_info = core.load_model_info(name)
     except ImportError, exc:
         print(str(exc))
Index: sasmodels/core.py
===================================================================
--- sasmodels/core.py	(revision c4993310e308c435dcf85328e919adef0ebc3ea1)
+++ sasmodels/core.py	(revision d19962c0471a0eee538ce03020b5b226ba2752f6)
@@ -24,9 +24,4 @@
     HAVE_OPENCL = False
 
-__all__ = [
-    "list_models", "load_model_info", "precompile_dll",
-    "build_model", "call_kernel", "call_ER_VR",
-]
-
 try:
     # Python 3.5 and up
@@ -44,4 +39,11 @@
         return module
 
+
+
+__all__ = [
+    "list_models", "load_model_info", "precompile_dll",
+    "build_model", "call_kernel", "call_ER_VR",
+]
+
 def list_models():
     """
@@ -65,27 +67,5 @@
     Load model info and build model.
     """
-    if model_name.endswith('.py'):
-        model_info = load_model_info_from_path(model_name)
-    else:
-        model_info = load_model_info(model_name)
-    return build_model(model_info, **kw)
-
-def load_model_info_from_path(path):
-    # Pull off the last .ext if it exists; there may be others
-    name = basename(splitext(path)[0])
-
-    # Not cleaning name since don't need to be able to reload this
-    # model later
-    # Should probably turf the model from sys.modules after we are done...
-
-    # Placing the model in the 'sasmodels.custom' name space, even
-    # though it doesn't actually exist.  imp.load_source doesn't seem
-    # to care.
-    import sasmodels.custom
-    kernel_module = load_module('sasmodels.custom.'+name, path)
-
-    # Now that we have the module, we can load it as usual
-    model_info = generate.make_model_info(kernel_module)
-    return model_info
+    return build_model(load_model_info(model_name), **kw)
 
 def load_model_info(model_name):
@@ -110,7 +90,20 @@
         return product.make_product_info(P_info, Q_info)
 
+    return make_model_by_name(model_name)
+
+
+def make_model_by_name(model_name):
+    if model_name.endswith('.py'):
+        path = model_name
+        # Pull off the last .ext if it exists; there may be others
+        name = basename(splitext(path)[0])
+        # Placing the model in the 'sasmodels.custom' name space.
+        from sasmodels import custom
+        kernel_module = load_module('sasmodels.custom.'+name, path)
+    else:
+        from sasmodels import models
+        __import__('sasmodels.models.'+model_name)
+        kernel_module = getattr(models, model_name, None)
     #import sys; print "\n".join(sys.path)
-    __import__('sasmodels.models.'+model_name)
-    kernel_module = getattr(models, model_name, None)
     return generate.make_model_info(kernel_module)
 
@@ -196,7 +189,5 @@
     """
     value = values.get(parameter.name, parameter.default)
-    if parameter.type not in ('volume', 'orientation'):
-        return [value], []
-    relative = parameter.type == 'volume'
+    relative = parameter.relative_pd
     limits = parameter.limits
     disperser = values.get(parameter.name+'_pd_type', 'gaussian')
@@ -204,4 +195,6 @@
     width = values.get(parameter.name+'_pd', 0.0)
     nsigma = values.get(parameter.name+'_pd_nsigma', 3.0)
+    if npts == 0 or width == 0:
+        return [value], []
     value, weight = weights.get_weights(
         disperser, npts, width, nsigma, value, limits, relative)
@@ -235,17 +228,17 @@
     *mono* is True if polydispersity should be set to none on all parameters.
     """
+    parameters = kernel.info['parameters']
     if mono:
         active = lambda name: False
     elif kernel.dim == '1d':
-        pars_1d = set(p.name for p in kernel.info['parameters'].type['1d'])
-        active = lambda name: name in pars_1d
+        active = lambda name: name in parameters.pd_1d
     elif kernel.dim == '2d':
-        pars_2d = set(p.name for p in kernel.info['parameters'].type['2d'])
-        active = lambda name: name in pars_2d
+        active = lambda name: name in parameters.pd_2d
     else:
         active = lambda name: True
 
-    vw_pairs = [(get_weights(p, pars) if active(p.name) else ([p.default], []))
-                for p in kernel.info['parameters']]
+    vw_pairs = [(get_weights(p, pars) if active(p.name)
+                 else ([pars.get(p.name, p.default)], []))
+                for p in parameters.call_parameters]
     values, weights = zip(*vw_pairs)
 
Index: sasmodels/generate.py
===================================================================
--- sasmodels/generate.py	(revision 5ff1b037170e74a37b38e653c5baeaa8cf54e08c)
+++ sasmodels/generate.py	(revision d19962c0471a0eee538ce03020b5b226ba2752f6)
@@ -184,5 +184,4 @@
 #__all__ = ["model_info", "make_doc", "make_source", "convert_type"]
 
-import sys
 from os.path import abspath, dirname, join as joinpath, exists, basename, \
     splitext, getmtime
@@ -198,6 +197,4 @@
 
 TEMPLATE_ROOT = dirname(__file__)
-
-MAX_PD = 4
 
 F16 = np.dtype('float16')
@@ -478,19 +475,15 @@
     source = [kernel_header] + user_code
 
-    vol_parameters = partable.kernel_pars('volume')
-    iq_parameters = partable.kernel_pars('1d')
-    iqxy_parameters = partable.kernel_pars('2d')
-
     # 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')]
     # Generate form_volume function, etc. from body only
     if model_info['form_volume'] is not None:
-        pars = vol_parameters
+        pars = partable.form_volume_parameters
         source.append(_gen_fn('form_volume', pars, model_info['form_volume']))
     if model_info['Iq'] is not None:
-        pars = [q] + iq_parameters
+        pars = [q] + partable.iq_parameters
         source.append(_gen_fn('Iq', pars, model_info['Iq']))
     if model_info['Iqxy'] is not None:
-        pars = [qx, qy] + iqxy_parameters
+        pars = [qx, qy] + partable.iqxy_parameters
         source.append(_gen_fn('Iqxy', pars, model_info['Iqxy']))
 
@@ -498,9 +491,9 @@
     source.append("#define PARAMETER_TABLE \\")
     source.append("\\\n".join(p.as_definition()
-                                  for p in model_info['parameters'][2:]))
+                              for p in partable.kernel_parameters))
 
     # Define the function calls
-    if vol_parameters:
-        refs = _call_pars("v.", vol_parameters)
+    if partable.form_volume_parameters:
+        refs = _call_pars("v.", partable.form_volume_parameters)
         call_volume = "#define CALL_VOLUME(v) form_volume(%s)" % (",".join(refs))
     else:
@@ -511,9 +504,9 @@
     source.append(call_volume)
 
-    refs = ["q[i]"] + _call_pars("v.", iq_parameters)
+    refs = ["q[i]"] + _call_pars("v.", partable.iq_parameters)
     call_iq = "#define CALL_IQ(q,i,v) Iq(%s)" % (",".join(refs))
     if _have_Iqxy(user_code):
         # Call 2D model
-        refs = ["q[2*i]", "q[2*i+1]"] + _call_pars("v.", iqxy_parameters)
+        refs = ["q[2*i]", "q[2*i+1]"] + _call_pars("v.", partable.iqxy_parameters)
         call_iqxy = "#define CALL_IQ(q,i,v) Iqxy(%s)" % (",".join(refs))
     else:
@@ -525,6 +518,6 @@
 
     # Fill in definitions for numbers of parameters
-    source.append("#define MAX_PD %s"%model_info['max_pd'])
-    source.append("#define NPARS %d"%(len(partable.kernel_pars())))
+    source.append("#define MAX_PD %s"%partable.max_pd)
+    source.append("#define NPARS %d"%partable.npars)
 
     # TODO: allow mixed python/opencl kernels?
@@ -546,23 +539,4 @@
     return '\n'.join(source)
 
-def categorize_parameters(pars):
-    """
-    Categorize the parameters by use:
-
-    * *pd* list of polydisperse parameters in order; gui should test whether
-      they are in *2d* or *magnetic* as appropriate for the data
-    * *1d* set of parameters that are used to compute 1D patterns
-    * *2d* set of parameters that are used to compute 2D patterns (which
-      includes all 1D parameters)
-    * *magnetic* set of parameters that are used to compute magnetic
-      patterns (which includes all 1D and 2D parameters)
-    * *pd_relative* is the set of parameters with relative distribution
-      width (e.g., radius +/- 10%) rather than absolute distribution
-      width (e.g., theta +/- 6 degrees).
-    * *theta_par* is the index of the polar angle polydispersion parameter
-      or -1 if no such parameter exists
-    """
-    par_set = {}
-
 def process_parameters(model_info):
     """
@@ -573,14 +547,9 @@
         model_info['demo'] = partable.defaults
 
-    # Don't use more polydisperse parameters than are available in the model
-    # Note: we can do polydispersity on arbitrary parameters, so it is not
-    # clear that this is a good idea; it does however make the poly_details
-    # code easier to write, so we will leave it in for now.
-    model_info['max_pd'] = min(partable.num_pd, MAX_PD)
-
 class CoordinationDetails(object):
     def __init__(self, model_info):
-        max_pd = model_info['max_pd']
-        npars = len(model_info['parameters'].kernel_pars())
+        parameters = model_info['parameters']
+        max_pd = parameters.max_pd
+        npars = parameters.npars
         par_offset = 4*max_pd
         self.details = np.zeros(par_offset + 3*npars + 4, 'i4')
@@ -596,5 +565,5 @@
 
         # theta_par is fixed
-        self.details[-1] = model_info['parameters'].theta_par
+        self.details[-1] = parameters.theta_offset
 
     @property
@@ -647,6 +616,6 @@
 
 def poly_details(model_info, weights):
-    weights = weights[2:]
-    max_pd = model_info['max_pd']
+    #print("weights",weights)
+    weights = weights[2:] # Skip scale and background
 
     # Decreasing list of polydispersity lengths
@@ -654,5 +623,5 @@
     pd_length = np.array([len(w) for w in weights])
     num_active = np.sum(pd_length>1)
-    if num_active > max_pd:
+    if num_active > model_info['parameters'].max_pd:
         raise ValueError("Too many polydisperse parameters")
 
@@ -745,8 +714,4 @@
       *model_info* blocks for the composition objects.  This allows us to
       build complete product and mixture models from just the info.
-    * *max_pd* is the max polydispersity dimension.  This is constant and
-      should not be reset.  You may be able to change it when the program
-      starts by setting *sasmodels.generate.MAX_PD*.
-
     """
     # TODO: maybe turn model_info into a class ModelDefinition
@@ -837,5 +802,4 @@
 
 
-
 def demo_time():
     """
@@ -853,12 +817,11 @@
     Program which prints the source produced by the model.
     """
+    import sys
+    from sasmodels.core import make_model_by_name
     if len(sys.argv) <= 1:
         print("usage: python -m sasmodels.generate modelname")
     else:
         name = sys.argv[1]
-        import sasmodels.models
-        __import__('sasmodels.models.' + name)
-        model = getattr(sasmodels.models, name)
-        model_info = make_model_info(model)
+        model_info = make_model_by_name(name)
         source = make_source(model_info)
         print(source)
Index: sasmodels/kerneldll.py
===================================================================
--- sasmodels/kerneldll.py	(revision 5ff1b037170e74a37b38e653c5baeaa8cf54e08c)
+++ sasmodels/kerneldll.py	(revision d19962c0471a0eee538ce03020b5b226ba2752f6)
@@ -266,5 +266,4 @@
         assert weights.dtype == real and values.dtype == real
 
-        max_pd = self.info['max_pd']
         start, stop = 0, details.total_pd
         #print("in kerneldll")
Index: sasmodels/modelinfo.py
===================================================================
--- sasmodels/modelinfo.py	(revision c4993310e308c435dcf85328e919adef0ebc3ea1)
+++ sasmodels/modelinfo.py	(revision d19962c0471a0eee538ce03020b5b226ba2752f6)
@@ -4,4 +4,6 @@
 # TODO: turn ModelInfo into a proper class
 ModelInfo = dict
+
+MAX_PD = 4
 
 COMMON_PARAMETERS = [
@@ -153,4 +155,5 @@
     *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
@@ -163,4 +166,5 @@
         self.id = name.split('[')[0].strip()
         self.name = name
+        self.units = units
         self.default = default
         self.limits = limits
@@ -169,9 +173,9 @@
         self.choices = None
 
-        # Length and length_control will be filled in by
-        # set_vector_length_from_reference(partable) once the complete
+        # Length and length_control will be filled in once the complete
         # parameter table is available.
         self.length = 1
         self.length_control = None
+        self.is_control = False
 
         # TODO: need better control over whether a parameter is polydisperse
@@ -216,24 +220,89 @@
         return "P<%s>"%self.name
 
+
 class ParameterTable(object):
+    """
+    ParameterTable manages the list of available parameters.
+
+    There are a couple of complications which mean that the list of parameters
+    for the kernel differs from the list of parameters that the user sees.
+
+    (1) Common parameters.  Scale and background are implicit to every model,
+    but are not passed to the kernel.
+
+    (2) Vector parameters.  Vector parameters are passed to the kernel as a
+    pointer to an array, e.g., thick[], but they are seen by the user as n
+    separate parameters thick1, thick2, ...
+
+    Therefore, the parameter table is organized by how it is expected to be
+    used. The following information is needed to set up the kernel functions:
+
+    * *kernel_parameters* is the list of parameters in the kernel parameter
+    table, with vector parameter p declared as p[].
+
+    * *iq_parameters* is the list of parameters to the Iq(q, ...) function,
+    with vector parameter p sent as p[].
+
+    * *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[].
+
+    Problem details, which sets up the polydispersity loops, requires the
+    following:
+
+    * *theta_offset* is the offset of the theta parameter in the kernel parameter
+    table, with vector parameters counted as n individual parameters
+    p1, p2, ..., or offset is -1 if there is no theta parameter.
+
+    * *max_pd* is the maximum number of polydisperse parameters, with vector
+    parameters counted as n individual parameters p1, p2, ...  Note that
+    this number is limited to sasmodels.modelinfo.MAX_PD.
+
+    * *npars* is the total number of parameters to the kernel, with vector
+    parameters counted as n individual parameters p1, p2, ...
+
+    * *call_parameters* is the complete list of parameters to the kernel,
+    including scale and background, with vector parameters recorded as
+    individual parameters p1, p2, ...
+
+    * *active_1d* is the set of names that may be polydisperse for 1d data
+
+    * *active_2d* is the set of names that may be polydisperse for 2d data
+
+    User parameters are the set of parameters visible to the user, including
+    the scale and background parameters that the kernel does not see.  User
+    parameters don't use vector notation, and instead use p1, p2, ...
+
+    * *control_parameters* is the
+
+    """
     # scale and background are implicit parameters
     COMMON = [Parameter(*p) for p in COMMON_PARAMETERS]
 
     def __init__(self, parameters):
-        self.parameters = self.COMMON + parameters
-        self._name_table= dict((p.name, p) for p in parameters)
+        self.kernel_parameters = parameters
+        self._set_vector_lengths()
+        self._make_call_parameter_list()
         self._categorize_parameters()
-
-        self._set_vector_lengths()
         self._set_defaults()
+        #self._name_table= dict((p.id, p) for p in parameters)
 
     def _set_vector_lengths(self):
         # Sort out the length of the vector parameters such as thickness[n]
-        for p in self.parameters:
+        for p in self.kernel_parameters:
             if p.length_control:
-                ref = self._name_table[p.length_control]
+                for ref in self.kernel_parameters:
+                    if ref.id == p.length_control:
+                        break
+                else:
+                    raise ValueError("no reference variable %r for %s"
+                                     % (p.length_control, p.name))
+                ref.is_control = True
                 low, high = ref.limits
                 if int(low) != low or int(high) != high or low<0 or high>20:
-                    raise ValueError("expected limits on %s to be within [0, 20]"%ref.name)
+                    raise ValueError("expected limits on %s to be within [0, 20]"
+                                     % ref.name)
                 p.length = high
 
@@ -241,12 +310,27 @@
         # Construct default values, including vector defaults
         defaults = {}
-        for p in self.parameters:
+        for p in self.call_parameters:
             if p.length == 1:
                 defaults[p.id] = p.default
             else:
-                for k in range(p.length):
-                    defaults["%s[%d]"%(p.id, k)] = p.default
+                for k in range(1, p.length+1):
+                    defaults["%s%d"%(p.id, k)] = p.default
         self.defaults = defaults
 
+    def _make_call_parameter_list(self):
+        full_list = self.COMMON[:]
+        for p in self.kernel_parameters:
+            if p.length == 1:
+                full_list.append(p)
+            else:
+                for k in range(1, p.length+1):
+                    pk = Parameter(p.id+str(k), p.units, p.default,
+                                   p.limits, p.type, p.description)
+                    pk.polydisperse = p.polydisperse
+                    pk.relative_pd = p.relative_pd
+                    full_list.append(pk)
+        self.call_parameters = full_list
+
+    """ # Suppress these for now until we see how they are used
     def __getitem__(self, k):
         if isinstance(k, (int, slice)):
@@ -259,71 +343,130 @@
 
     def __iter__(self):
-        return iter(self.parameters)
-
-    def kernel_pars(self, ptype=None):
-        """
-        Return the parameters to the user kernel which match the given type.
-
-        Types include '1d' for Iq kernels, '2d' for Iqxy kernels and
-        'volume' for form_volume kernels.
-        """
-        # Assumes background and scale are the first two parameters
-        if ptype is None:
-            return self.parameters[2:]
+        return iter(self.expanded_parameters)
+    """
+
+    def _categorize_parameters(self):
+        # Set the kernel parameters.  Assumes background and scale are the
+        # first two parameters in the parameter list, but these are not sent
+        # to the underlying kernel functions.
+        self.iq_parameters = [p for p in self.kernel_parameters
+                              if p.type not in ('orientation', 'magnetic')]
+        self.iqxy_parameters = [p for p in self.kernel_parameters
+                                if p.type != 'magnetic']
+        self.form_volume_parameters = [p for p in self.kernel_parameters
+                                       if p.type == 'volume']
+
+        # Theta offset
+        offset = 0
+        for p in self.kernel_parameters:
+            if p.name == 'theta':
+                self.theta_offset = offset
+                break
+            offset += p.length
         else:
-            return [p for p in self.parameters[2:] if p in self.type[ptype]]
-
-    def _categorize_parameters(self):
-        """
-        Build parameter categories out of the the parameter definitions.
-
-        Returns a dictionary of categories.
-
-        Note: these categories are subject to change, depending on the needs of
-        the UI and the needs of the kernel calling function.
-
-        The categories are as follows:
-
-        * *volume* list of volume parameter names
-        * *orientation* list of orientation parameters
-        * *magnetic* list of magnetic parameters
-        * *sld* list of parameters that have no type info
-        * *other* list of parameters that have no type info
-
-        Each parameter is in one and only one category.
-        """
-        pars = self.parameters
-
-        par_type = {
-            'volume': [], 'orientation': [], 'magnetic': [], 'sld': [], 'other': [],
-        }
-        for p in self.parameters:
-            par_type[p.type if p.type else 'other'].append(p)
-        par_type['1d'] = [p for p in pars if p.type not in ('orientation', 'magnetic')]
-        par_type['2d'] = [p for p in pars if p.type != 'magnetic']
-        par_type['pd'] = [p for p in pars if p.polydisperse]
-        par_type['pd_relative'] = [p for p in pars if p.relative_pd]
-        self.type = par_type
-
-        # find index of theta (or whatever variable is used for spherical
-        # normalization during polydispersity...
-        if 'theta' in par_type['2d']:
-            # TODO: may be an off-by 2 bug due to background and scale
-            # TODO: is theta always the polar coordinate?
-            self.theta_par = [k for k,p in enumerate(pars) if p.name=='theta'][0]
-        else:
-            self.theta_par = -1
-
-    @property
-    def num_pd(self):
-        """
-        Number of distributional parameters in the model (polydispersity in
-        shape dimensions and orientational distributions).
-        """
-        return sum(p.length for p in self.type['pd'])
-
-    @property
-    def has_2d(self):
-        return self.type['orientation'] or self.type['magnetic']
+            self.theta_offset = -1
+
+        # number of polydisperse parameters
+        num_pd = sum(p.length for p in self.kernel_parameters if p.polydisperse)
+        # Don't use more polydisperse parameters than are available in the model
+        # Note: we can do polydispersity on arbitrary parameters, so it is not
+        # clear that this is a good idea; it does however make the poly_details
+        # code easier to write, so we will leave it in for now.
+        self.max_pd = min(num_pd, MAX_PD)
+
+        self.npars = sum(p.length for p in self.kernel_parameters)
+
+        # true if has 2D parameters
+        self.has_2d = any(p.type in ('orientation', 'magnetic')
+                          for p in self.kernel_parameters)
+
+        self.pd_1d = set(p.name for p in self.call_parameters
+                if p.polydisperse and p.type not in ('orientation', 'magnetic'))
+        self.pd_2d = set(p.name for p in self.call_parameters
+                         if p.polydisperse and p.type != 'magnetic')
+
+    def user_parameters(self, pars, is2d):
+        """
+        Return the list of parameters for the given data type.
+
+        Vector parameters are expanded as in place.  If multiple parameters
+        share the same vector length, then the parameters will be interleaved
+        in the result.  The control parameters come first.  For example,
+        if the parameter table is ordered as::
+
+            sld_core
+            sld_shell[num_shells]
+            sld_solvent
+            thickness[num_shells]
+            num_shells
+
+        and *pars[num_shells]=2* then the returned list will be::
+
+            num_shells
+            scale
+            background
+            sld_core
+            sld_shell1
+            thickness1
+            sld_shell2
+            thickness2
+            sld_solvent
+
+        Note that shell/thickness pairs are grouped together in the result
+        even though they were not grouped in the incoming table.  The control
+        parameter is always returned first since the GUI will want to set it
+        early, and rerender the table when it is changed.
+        """
+        control = [p for p in self.kernel_parameters if p.is_control]
+
+        # Gather entries such as name[n] into groups of the same n
+        dependent = dict((p.id, []) for p in control)
+        for p in self.kernel_parameters:
+            if p.length_control is not None:
+                dependent[p.length_control].append(p)
+
+        # Gather entries such as name[4] into groups of the same length
+        fixed = {}
+        for p in self.kernel_parameters:
+            if p.length > 1 and p.length_control is None:
+                fixed.setdefault(p.length, []).append(p)
+
+        # Using the call_parameters table, we already have expanded forms
+        # for each of the vector parameters; put them in a lookup table
+        expanded_pars = dict((p.name, p) for p in self.call_parameters)
+
+        # Gather the user parameters in order
+        result = control + self.COMMON
+        for p in self.kernel_parameters:
+            if not is2d and p.type in ('orientation', 'magnetic'):
+                pass
+            elif p.is_control:
+                pass # already added
+            elif p.length_control is not None:
+                table = dependent.get(p.length_control, [])
+                if table:
+                    # look up length from incoming parameters
+                    table_length = int(pars[p.length_control])
+                    del dependent[p.length_control] # first entry seen
+                    for k in range(1, table_length+1):
+                        for entry in table:
+                            result.append(expanded_pars[entry.id+str(k)])
+                else:
+                    pass # already processed all entries
+            elif p.length > 1:
+                table = fixed.get(p.length, [])
+                if table:
+                    table_length = p.length
+                    del fixed[p.length]
+                    for k in range(1, table_length+1):
+                        for entry in table:
+                            result.append(expanded_pars[entry.id+str(k)])
+                else:
+                    pass # already processed all entries
+            else:
+                result.append(p)
+
+        return result
+
 
 
