Index: sasmodels/compare.py
===================================================================
--- sasmodels/compare.py	(revision a4e2ae5d9fb8933e2d59b124aadd479a388e96ac)
+++ sasmodels/compare.py	(revision 69aa451cd7078d77c255be72d487793b00b34eee)
@@ -870,7 +870,7 @@
         pars, pd_types = bumps_model.create_parameters(model_info, **opts['pars'])
         if not opts['is2d']:
-            for name in model_info['par_type']['1d']:
+            for p in model_info['parameters'].type['1d']:
                 for ext in ['', '_pd', '_pd_n', '_pd_nsigma']:
-                    k = name+ext
+                    k = p.name+ext
                     v = pars.get(k, None)
                     if v is not None:
Index: sasmodels/core.py
===================================================================
--- sasmodels/core.py	(revision fec69dd09a0e9893e1cb253c163d9efbd4d4db7a)
+++ sasmodels/core.py	(revision 69aa451cd7078d77c255be72d487793b00b34eee)
@@ -218,7 +218,9 @@
         active = lambda name: False
     elif kernel.dim == '1d':
-        active = lambda name: name in set(kernel.info['par_type']['1d'])
+        pars_1d = set(p.name for p in kernel.info['parameters'].type['1d'])
+        active = lambda name: name in pars_1d
     elif kernel.dim == '2d':
-        active = lambda name: name in set(kernel.info['par_type']['2d'])
+        pars_2d = set(p.name for p in kernel.info['parameters'].type['2d'])
+        active = lambda name: name in pars_1d
     else:
         active = lambda name: True
Index: sasmodels/generate.py
===================================================================
--- sasmodels/generate.py	(revision ea1f14da54eb4e416cb5114e43add10dba3fc6d0)
+++ sasmodels/generate.py	(revision 69aa451cd7078d77c255be72d487793b00b34eee)
@@ -69,39 +69,4 @@
 The constructor code will generate the necessary vectors for computing
 them with the desired polydispersity.
-
-The available kernel parameters are defined as a list, with each parameter
-defined as a sublist with the following elements:
-
-    *name* is the name that will be used in the call to the kernel
-    function and the name that will be displayed to the user.  Names
-    should be lower case, with words separated by underscore.  If
-    acronyms are used, the whole acronym should be upper case.
-
-    *units* should be one of *degrees* for angles, *Ang* for lengths,
-    *1e-6/Ang^2* for SLDs.
-
-    *default value* will be the initial value for  the model when it
-    is selected, or when an initial value is not otherwise specified.
-
-    *limits = [lb, ub]* are the hard limits on the parameter value, used to
-    limit the polydispersity density function.  In the fit, the parameter limits
-    given to the fit are the limits  on the central value of the parameter.
-    If there is polydispersity, it will evaluate parameter values outside
-    the fit limits, but not outside the hard limits specified in the model.
-    If there are no limits, use +/-inf imported from numpy.
-
-    *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
-    be used in all of *Iq*, *Iqxy* and *Imagnetic*.  "sld" parameters
-    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.
-
-    *description* is a short description of the parameter.  This will
-    be displayed in the parameter table and used as a tool tip for the
-    parameter value in the user interface.
-
 The kernel module must set variables defining the kernel meta data:
 
@@ -216,5 +181,6 @@
 from __future__ import print_function
 
-# TODO: identify model files which have changed since loading and reload them.
+#TODO: determine which functions are useful outside of generate
+#__all__ = ["model_info", "make_doc", "make_source", "convert_type"]
 
 import sys
@@ -224,14 +190,10 @@
 import string
 import warnings
-from collections import namedtuple
 
 import numpy as np
 
-# TODO: promote Parameter and model_info to classes
-PARAMETER_FIELDS = ['name', 'units', 'default', 'limits', 'type', 'description']
-Parameter = namedtuple('Parameter', PARAMETER_FIELDS)
-
-#TODO: determine which functions are useful outside of generate
-#__all__ = ["model_info", "make_doc", "make_source", "convert_type"]
+from .modelinfo import ModelInfo, Parameter, make_parameter_table
+
+# TODO: identify model files which have changed since loading and reload them.
 
 TEMPLATE_ROOT = dirname(__file__)
@@ -246,10 +208,4 @@
 except TypeError:
     F128 = None
-
-# Scale and background, which are parameters common to every form factor
-COMMON_PARAMETERS = [
-    ["scale", "", 1, [0, np.inf], "", "Source intensity"],
-    ["background", "1/cm", 1e-3, [0, np.inf], "", "Source background"],
-    ]
 
 # Conversion from units defined in the parameter table for each model
@@ -354,4 +310,15 @@
     return [_search(search_path, f) for f in model_info['source']]
 
+def timestamp(model_info):
+    """
+    Return a timestamp for the model corresponding to the most recently
+    changed file or dependency.
+    """
+    source_files = (model_sources(model_info)
+                    + model_templates()
+                    + [model_info['filename']])
+    newest = max(getmtime(f) for f in source_files)
+    return newest
+
 def convert_type(source, dtype):
     """
@@ -448,5 +415,5 @@
          }
     """
-    par_decl = ', '.join('double ' + p for p in pars) if pars else 'void'
+    par_decl = ', '.join(p.as_argument() for p in pars) if pars else 'void'
     return _FN_TEMPLATE % {'name': name, 'body': body, 'pars': par_decl}
 
@@ -455,6 +422,5 @@
     Return a list of *prefix.parameter* from parameter items.
     """
-    prefix += "."
-    return [prefix+p for p in pars]
+    return [p.as_call_reference(prefix) for p in pars]
 
 _IQXY_PATTERN = re.compile("^((inline|static) )? *(double )? *Iqxy *([(]|$)",
@@ -499,13 +465,8 @@
     # for computing volume even if we allow non-disperse volume parameters.
 
-    # kernel_iq assumes scale and background are the first parameters;
-    # they should be first for 1d and 2d parameter lists as well.
-    assert model_info['parameters'][0].name == 'scale'
-    assert model_info['parameters'][1].name == 'background'
-
-    # Identify parameter types
-    iq_parameters = model_info['par_type']['1d'][2:]
-    iqxy_parameters = model_info['par_type']['2d'][2:]
-    vol_parameters = model_info['par_type']['volume']
+    partable = model_info['parameters']
+
+    # Identify parameters for Iq, Iqxy, Iq_magnetic and form_volume.
+    # Note that scale and volume are not possible types.
 
     # Load templates and user code
@@ -517,24 +478,30 @@
     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:
-        pnames = [p for p in vol_parameters]
-        source.append(_gen_fn('form_volume', pnames, model_info['form_volume']))
+        pars = vol_parameters
+        source.append(_gen_fn('form_volume', pars, model_info['form_volume']))
     if model_info['Iq'] is not None:
-        pnames = ['q'] + [p for p in iq_parameters]
-        source.append(_gen_fn('Iq', pnames, model_info['Iq']))
+        pars = [q] + iq_parameters
+        source.append(_gen_fn('Iq', pars, model_info['Iq']))
     if model_info['Iqxy'] is not None:
-        pnames = ['qx', 'qy'] + [p for p in iqxy_parameters]
-        source.append(_gen_fn('Iqxy', pnames, model_info['Iqxy']))
+        pars = [qx, qy] + iqxy_parameters
+        source.append(_gen_fn('Iqxy', pars, model_info['Iqxy']))
 
     # Define the parameter table
     source.append("#define PARAMETER_TABLE \\")
-    source.append("\\\n    ".join("double %s;"%p.name
-                                   for p in model_info['parameters'][2:]))
+    source.append("\\\n".join(p.as_definition()
+                                  for p in model_info['parameters'][2:]))
 
     # Define the function calls
     if vol_parameters:
-        refs = ",".join(_call_pars("v", vol_parameters))
-        call_volume = "#define CALL_VOLUME(v) form_volume(%s)"%refs
+        refs = _call_pars("v.", vol_parameters)
+        call_volume = "#define CALL_VOLUME(v) form_volume(%s)" % (",".join(refs))
     else:
         # Model doesn't have volume.  We could make the kernel run a little
@@ -544,9 +511,9 @@
     source.append(call_volume)
 
-    refs = ["q[i]"] + _call_pars("v", iq_parameters)
+    refs = ["q[i]"] + _call_pars("v.", 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.", iqxy_parameters)
         call_iqxy = "#define CALL_IQ(q,i,v) Iqxy(%s)" % (",".join(refs))
     else:
@@ -559,5 +526,5 @@
     # Fill in definitions for numbers of parameters
     source.append("#define MAX_PD %s"%model_info['max_pd'])
-    source.append("#define NPARS %d"%(len(model_info['parameters'])-2))
+    source.append("#define NPARS %d"%(len(partable.kernel_pars())))
 
     # TODO: allow mixed python/opencl kernels?
@@ -597,43 +564,4 @@
     """
     par_set = {}
-    par_set['1d'] = [p.name for p in pars if p.type not in ('orientation', 'magnetic')]
-    par_set['2d'] = [p.name for p in pars if p.type != 'magnetic']
-    par_set['magnetic'] = [p.name for p in pars]
-    par_set['pd'] = [p.name for p in pars if p.type in ('volume', 'orientation')]
-    par_set['pd_relative'] = [p.name for p in pars if p.type == 'volume']
-    if 'theta' in par_set['2d']:
-        # find id of theta in parameter set (or whatever variable is
-        # used for spherical normalization during polydispersity...
-        par_set['theta_par'] = [k for k,p in enumerate(pars) if p.name=='theta'][0]
-    else:
-        par_set['theta_par'] = -1
-    return par_set
-
-def collect_types(pars):
-    """
-    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.
-    """
-    par_type = {
-        'volume': [], 'orientation': [], 'magnetic': [], 'sld': [], 'other': [],
-    }
-    for p in pars:
-        par_type[p.type if p.type else 'other'].append(p.name)
-    return  par_type
-
 
 def process_parameters(model_info):
@@ -641,33 +569,23 @@
     Process parameter block, precalculating parameter details.
     """
-    # convert parameters into named tuples
-    for p in model_info['parameters']:
-        if p[4] == '' and (p[0].startswith('sld') or p[0].endswith('sld')):
-            p[4] = 'sld'
-            # TODO: make sure all models explicitly label their sld parameters
-            #raise ValueError("%s.%s needs to be explicitly set to type 'sld'" %(model_info['id'], p[0]))
-
-    pars = [Parameter(*p) for p in model_info['parameters']]
-    # Fill in the derived attributes
-    par_type = collect_types(pars)
-    par_type.update(categorize_parameters(pars))
-    model_info['parameters'] = pars
-    model_info['par_type'] = par_type
+    partable = model_info['parameters']
     if model_info.get('demo', None) is None:
-        model_info['demo'] = dict((p.name, p.default) for p in pars)
-    model_info['has_2d'] = par_type['orientation'] or par_type['magnetic']
+        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(len(par_type['pd']), MAX_PD)
+    model_info['max_pd'] = min(partable.num_pd, MAX_PD)
 
 def mono_details(model_info):
+    # TODO: move max_pd into ParameterTable?
     max_pd = model_info['max_pd']
-    npars = len(model_info['parameters']) - 2
-    p = 5*max_pd
-    c = p + 3*npars
-
-    details = np.zeros(c + 2, 'int32')
+    pars = model_info['parameters'].kernel_pars()
+    npars = len(pars)
+    par_offset = 5*max_pd
+    constants_offset = par_offset + 3*npars
+
+    details = np.zeros(constants_offset + 2, 'int32')
     details[0*max_pd:1*max_pd] = range(max_pd)       # pd_par: arbitrary order; use first
     details[1*max_pd:2*max_pd] = [1]*max_pd          # pd_length: only one element
@@ -675,20 +593,19 @@
     details[3*max_pd:4*max_pd] = [1]*max_pd          # pd_stride: vectors of length 1
     details[4*max_pd:5*max_pd] = [0]*max_pd          # pd_isvol: doens't matter if no norm
-    details[p+0*npars:p+1*npars] = range(2, npars+2) # par_offset: skip scale and background
-    details[p+1*npars:p+2*npars] = [0]*npars         # no coordination
+    details[par_offset+0*npars:par_offset+1*npars] = range(2, npars+2) # par_offset: skip scale and background
+    details[par_offset+1*npars:par_offset+2*npars] = [0]*npars         # no coordination
     #details[p+npars] = 1 # par_coord[0] is coordinated with the first par?
-    details[p+2*npars:p+3*npars] = 0 # fast coord with 0
-    details[c]   = 1     # fast_coord_count: one fast index
-    details[c+1] = -1    # theta_par: None
+    details[par_offset+2*npars:par_offset+3*npars] = 0 # fast coord with 0
+    details[constants_offset]   = 1     # fast_coord_count: one fast index
+    details[constants_offset+1] = -1    # theta_par: None
     return details
 
 def poly_details(model_info, weights):
-    print("entering poly",weights)
-
-    print([p.name for p in model_info['parameters']])
-    pars = model_info['parameters'][2:]  # skip scale and background
     weights = weights[2:]
+
+    # TODO: move max_pd into ParameterTable?
     max_pd = model_info['max_pd']
-    npars = len(pars) # scale and background already removed
+    pars = model_info['parameters'].kernel_pars
+    npars = len(pars)
     par_offset = 5*max_pd
     constants_offset = par_offset + 3*npars
@@ -745,5 +662,6 @@
     """
     if model_info['Iq'] is not None and model_info['Iqxy'] is None:
-        if model_info['par_type']['1d'] != model_info['par_type']['2d']:
+        partable = model_info['parameters']
+        if partable.type['1d'] != partable.type['2d']:
             raise ValueError("Iqxy model is missing")
         Iq = model_info['Iq']
@@ -751,4 +669,5 @@
             return Iq(np.sqrt(qx**2 + qy**2), **kw)
         model_info['Iqxy'] = Iqxy
+
 
 def make_model_info(kernel_module):
@@ -800,5 +719,6 @@
     """
     # TODO: maybe turn model_info into a class ModelDefinition
-    parameters = COMMON_PARAMETERS + kernel_module.parameters
+    #print("make parameter table", kernel_module.parameters)
+    parameters = make_parameter_table(kernel_module.parameters)
     filename = abspath(kernel_module.__file__)
     kernel_id = splitext(basename(filename))[0]
Index: sasmodels/kernel_iq.c
===================================================================
--- sasmodels/kernel_iq.c	(revision fec69dd09a0e9893e1cb253c163d9efbd4d4db7a)
+++ sasmodels/kernel_iq.c	(revision 69aa451cd7078d77c255be72d487793b00b34eee)
@@ -14,6 +14,4 @@
 #ifndef _PAR_BLOCK_ // protected block so we can include this code twice.
 #define _PAR_BLOCK_
-
-#define MAX_PD 4  // MAX_PD is the max number of polydisperse parameters
 
 typedef struct {
Index: sasmodels/kerneldll.py
===================================================================
--- sasmodels/kerneldll.py	(revision fec69dd09a0e9893e1cb253c163d9efbd4d4db7a)
+++ sasmodels/kerneldll.py	(revision 69aa451cd7078d77c255be72d487793b00b34eee)
@@ -140,9 +140,6 @@
 
     source = generate.convert_type(source, dtype)
-    source_files = (generate.model_sources(model_info)
-                    + [model_info['filename']]
-                    + generate.model_templates())
+    newest = generate.timestamp(model_info)
     dll = dll_path(model_info, dtype)
-    newest = max(os.path.getmtime(f) for f in source_files)
     if not os.path.exists(dll) or os.path.getmtime(dll) < newest:
         # Replace with a proper temp file
Index: sasmodels/mixture.py
===================================================================
--- sasmodels/mixture.py	(revision 72a081dea6caa8fa493057133a6232f6b118c89a)
+++ sasmodels/mixture.py	(revision 69aa451cd7078d77c255be72d487793b00b34eee)
@@ -14,5 +14,5 @@
 import numpy as np
 
-from .generate import process_parameters, COMMON_PARAMETERS, Parameter
+from .modelinfo import Parameter, ParameterTable
 
 SCALE=0
@@ -34,19 +34,19 @@
 
     # Build new parameter list
-    pars = COMMON_PARAMETERS + []
+    pars = []
     for k, part in enumerate(parts):
         # Parameter prefix per model, A_, B_, ...
+        # Note that prefix must also be applied to id and length_control
+        # to support vector parameters
         prefix = chr(ord('A')+k) + '_'
-        for p in part['parameters']:
-            # No background on the individual mixture elements
-            if p.name == 'background':
-                continue
-            # TODO: promote Parameter to a full class
-            # this code knows too much about the implementation!
-            p = list(p)
-            if p[0] == 'scale':  # allow model subtraction
-                p[3] = [-np.inf, np.inf]  # limits
-            p[0] = prefix+p[0]   # relabel parameters with A_, ...
+        pars.append(Parameter(prefix+'scale'))
+        for p in part['parameters'].kernel_pars:
+            p = copy(p)
+            p.name = prefix+p.name
+            p.id = prefix+p.id
+            if p.length_control is not None:
+                p.length_control = prefix+p.length_control
             pars.append(p)
+    partable = ParameterTable(pars)
 
     model_info = {}
@@ -58,5 +58,5 @@
     model_info['docs'] = model_info['title']
     model_info['category'] = "custom"
-    model_info['parameters'] = pars
+    model_info['parameters'] = partable
     #model_info['single'] = any(part['single'] for part in parts)
     model_info['structure_factor'] = False
@@ -67,6 +67,4 @@
     # Remember the component info blocks so we can build the model
     model_info['composition'] = ('mixture', parts)
-    process_parameters(model_info)
-    return model_info
 
 
Index: sasmodels/modelinfo.py
===================================================================
--- sasmodels/modelinfo.py	(revision 69aa451cd7078d77c255be72d487793b00b34eee)
+++ sasmodels/modelinfo.py	(revision 69aa451cd7078d77c255be72d487793b00b34eee)
@@ -0,0 +1,325 @@
+
+import numpy as np
+
+# TODO: turn ModelInfo into a proper class
+ModelInfo = dict
+
+COMMON_PARAMETERS = [
+    ["scale", "", 1, [0, np.inf], "", "Source intensity"],
+    ["background", "1/cm", 1e-3, [0, np.inf], "", "Source background"],
+]
+assert (len(COMMON_PARAMETERS) == 2
+        and COMMON_PARAMETERS[0][0]=="scale"
+        and COMMON_PARAMETERS[1][0]=="background"), "don't change common parameters"
+# assumptions about common parameters exist throughout the code, such as:
+# (1) kernel functions Iq, Iqxy, 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
+#     with a scale that varies from [-inf, inf]
+# (4) product models drop the background and reassign scale
+# and maybe other places.
+# Note that scale and background cannot be coordinated parameters whose value
+# depends on the some polydisperse parameter with the current implementation
+
+def make_parameter_table(pars):
+    processed = []
+    for p in pars:
+        if not isinstance(p, list) or len(p) != 6:
+            raise ValueError("Parameter should be [name, units, default, limits, type, desc], but got %r"
+                             %str(p))
+        processed.append(parse_parameter(*p))
+    partable = ParameterTable(processed)
+    set_vector_length_from_reference(partable)
+    return partable
+
+def set_vector_length_from_reference(partable):
+    # Sort out the length of the vector parameters such as thickness[n]
+    for p in partable:
+        if p.length_control:
+           ref = partable[p.length_control]
+           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)
+           p.length = low
+
+def parse_parameter(name, units='', default=None,
+                    limits=(-np.inf, np.inf), type='', description=''):
+    # Parameter is a user facing class.  Do robust type checking.
+    if not isstr(name):
+        raise ValueError("expected string for parameter name %r"%name)
+    if not isstr(units):
+        raise ValueError("expected units to be a string for %s"%name)
+    # if limits is a list of strings, then this is a choice list
+    # field, and limits are 1 to length of string list
+    if isinstance(limits, list) and all(isstr(k) for k in limits):
+        choices = limits
+        limits = [1, len(choices)]
+    else:
+        choices = None
+    # TODO: maybe allow limits of None for (-inf, inf)
+    try:
+        low, high = limits
+        if not isinstance(low, (int, float)):
+            raise TypeError("low is not numeric")
+        if not isinstance(high, (int, float)):
+            raise TypeError("high is not numeric")
+        if low >= high:
+            raise ValueError("require low < high")
+    except:
+        raise ValueError("invalid limits %s for %s"%(limits, name))
+
+    if not isinstance(default, (int, float)):
+        raise ValueError("expected default %r to be a number for %s"
+                         % (default, name))
+    if default < low or default > high:
+        raise ValueError("default value %r not in range for %s"
+                         % (default, name))
+
+    if type not in ("volume", "orientation", "sld", "magnetic", ""):
+        raise ValueError("unexpected type %r for %s" % (type, name))
+
+    if not isstr(description):
+        raise ValueError("expected description to be a string")
+
+
+    # Parameter id for name[n] does not include [n]
+    if "[" in name:
+        if not name.endswith(']'):
+            raise ValueError("Expected name[len] for vector parameter %s"%name)
+        pid, ref = name[:-1].split('[', 1)
+        ref = ref.strip()
+    else:
+        pid, ref = name, None
+
+
+    # automatically identify sld types
+    if type=='' and (pid.startswith('sld') or pid.endswith('sld')):
+        type = 'sld'
+
+    # Check if using a vector definition, name[k], as the parameter name
+    if ref:
+        if ref == '':
+            raise ValueError("Need to specify vector length for %s"%name)
+        try:
+            length = int(ref)
+            control = None
+        except:
+            length = None
+            control = ref
+    else:
+        length = 1
+        control = None
+
+    # Build the parameter
+    parameter = Parameter(name=name, units=units, default=default,
+                          limits=limits, type=type, description=description)
+
+    # TODO: need better control over whether a parameter is polydisperse
+    parameter.polydisperse = type in ('orientation', 'volume')
+    parameter.relative_pd = type in ('volume')
+    parameter.choices = choices
+    parameter.length = length
+    parameter.length_control = control
+
+    return parameter
+
+class Parameter(object):
+    """
+    The available kernel parameters are defined as a list, with each parameter
+    defined as a sublist with the following elements:
+
+    *name* is the name that will be used in the call to the kernel
+    function and the name that will be displayed to the user.  Names
+    should be lower case, with words separated by underscore.  If
+    acronyms are used, the whole acronym should be upper case.
+
+    *units* should be one of *degrees* for angles, *Ang* for lengths,
+    *1e-6/Ang^2* for SLDs.
+
+    *default value* will be the initial value for  the model when it
+    is selected, or when an initial value is not otherwise specified.
+
+    *limits = [lb, ub]* are the hard limits on the parameter value, used to
+    limit the polydispersity density function.  In the fit, the parameter limits
+    given to the fit are the limits  on the central value of the parameter.
+    If there is polydispersity, it will evaluate parameter values outside
+    the fit limits, but not outside the hard limits specified in the model.
+    If there are no limits, use +/-inf imported from numpy.
+
+    *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
+    be used in all of *Iq*, *Iqxy* and *Imagnetic*.  "sld" parameters
+    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.
+
+    *description* is a short description of the parameter.  This will
+    be displayed in the parameter table and used as a tool tip for the
+    parameter value in the user interface.
+
+    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
+    *polydisperse* is true if the parameter accepts a polydispersity
+    *relative_pd* is true if that polydispersity is relative
+
+    In the usual process these values are set by :func:`make_parameter_table`
+    and :func:`parse_parameter` therein.
+    """
+    def __init__(self, name, units='', default=None, limits=(-np.inf, np.inf),
+                 type='', description=''):
+        self.id = name
+        self.name = name
+        self.default = default
+        self.limits = limits
+        self.type = type
+        self.description = description
+        self.choices = None
+
+        # Length and length_control will be filled in by
+        # set_vector_length_from_reference(partable) once the complete
+        # parameter table is available.
+        self.length = 1
+        self.length_control = None
+
+        # TODO: need better control over whether a parameter is polydisperse
+        self.polydisperse = False
+        self.relative_pd = False
+
+    def as_definition(self):
+        """
+        Declare space for the variable in a parameter structure.
+
+        For example, the parameter thickness with length 3 will
+        return "double thickness[3];", with no spaces before and
+        no new line character afterward.
+        """
+        if self.length == 1:
+            return "double %s;"%self.id
+        else:
+            return "double %s[%d];"%(self.id, self.length)
+
+    def as_function_argument(self):
+        """
+        Declare the variable as a function argument.
+
+        For example, the parameter thickness with length 3 will
+        return "double *thickness", with no spaces before and
+        no comma afterward.
+        """
+        if self.length == 1:
+            return "double %s"%self.id
+        else:
+            return "double *%s"%self.id
+
+    def as_call_reference(self, prefix=""):
+        # Note: if the parameter is a struct type, then we will need to use
+        # &prefix+id.  For scalars and vectors we can just use prefix+id.
+        return prefix + self.id
+
+    def __str__(self):
+        return "<%s>"%self.name
+
+    def __repr__(self):
+        return "P<%s>"%self.name
+
+class ParameterTable(object):
+    # 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._categorize_parameters()
+
+    def __getitem__(self, k):
+        if isinstance(k, (int, slice)):
+            return self.parameters[k]
+        else:
+            return self._name_table[k]
+
+    def __contains__(self, key):
+        return key in self._name_table
+
+    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:]
+        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['magnetic'] = [p for p in pars]
+        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 defaults(self):
+        return dict((p.name, p.default) for p in self.parameters)
+
+    @property
+    def num_pd(self):
+        """
+        Number of distributional parameters in the model (polydispersity in
+        shape dimensions and orientational distributions).
+        """
+        return len(self.type['pd'])
+
+    @property
+    def has_2d(self):
+        return self.type['orientation'] or self.type['magnetic']
+
+
+def isstr(x):
+    # TODO: 2-3 compatible tests for str, including unicode strings
+    return isinstance(x, str)
+
Index: sasmodels/models/rpa.py
===================================================================
--- sasmodels/models/rpa.py	(revision aa2edb2b2fb550caed0506dcf63bc7eac7e1779a)
+++ sasmodels/models/rpa.py	(revision 69aa451cd7078d77c255be72d487793b00b34eee)
@@ -86,29 +86,11 @@
 #   ["name", "units", default, [lower, upper], "type","description"],
 parameters = [
-    ["case_num", CASES, 0, [0, 10], "", "Component organization"],
+    ["case_num", "", 1, CASES, "", "Component organization"],
 
-    ["Na", "", 1000.0, [1, inf], "", "Degree of polymerization"],
-    ["Phia", "", 0.25, [0, 1], "", "volume fraction"],
-    ["va", "mL/mol", 100.0, [0, inf], "", "specific volume"],
-    ["La", "fm", 10.0, [-inf, inf], "", "scattering length"],
-    ["ba", "Ang", 5.0, [0, inf], "", "segment length"],
-
-    ["Nb", "", 1000.0, [1, inf], "", "Degree of polymerization"],
-    ["Phib", "", 0.25, [0, 1], "", "volume fraction"],
-    ["vb", "mL/mol", 100.0, [0, inf], "", "specific volume"],
-    ["Lb", "fm", 10.0, [-inf, inf], "", "scattering length"],
-    ["bb", "Ang", 5.0, [0, inf], "", "segment length"],
-
-    ["Nc", "", 1000.0, [1, inf], "", "Degree of polymerization"],
-    ["Phic", "", 0.25, [0, 1], "", "volume fraction"],
-    ["vc", "mL/mol", 100.0, [0, inf], "", "specific volume"],
-    ["Lc", "fm", 10.0, [-inf, inf], "", "scattering length"],
-    ["bc", "Ang", 5.0, [0, inf], "", "segment length"],
-
-    ["Nd", "", 1000.0, [1, inf], "", "Degree of polymerization"],
-    ["Phid", "", 0.25, [0, 1], "", "volume fraction"],
-    ["vd", "mL/mol", 100.0, [0, inf], "", "specific volume"],
-    ["Ld", "fm", 10.0, [-inf, inf], "", "scattering length"],
-    ["bd", "Ang", 5.0, [0, inf], "", "segment length"],
+    ["N[4]", "", 1000.0, [1, inf], "", "Degree of polymerization"],
+    ["Phi[4]", "", 0.25, [0, 1], "", "volume fraction"],
+    ["v[4]", "mL/mol", 100.0, [0, inf], "", "specific volume"],
+    ["L[4]", "fm", 10.0, [-inf, inf], "", "scattering length"],
+    ["b[4]", "Ang", 5.0, [0, inf], "", "segment length"],
 
     ["Kab", "", -0.0004, [-inf, inf], "", "Interaction parameter"],
