Index: sasmodels/bumps_model.py
===================================================================
--- sasmodels/bumps_model.py	(revision a84a0cac83cecece77ff644df4f2552ef1452b62)
+++ sasmodels/bumps_model.py	(revision 303d8d66300d33bd04a1db60bb002b734d6f6985)
@@ -81,9 +81,9 @@
     from bumps.names import Parameter
 
-    pars = {}
+    pars = {} # => floating point parameters
     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['partype']['pd-2d']:
+    for name in model_info['par_type']['pd']:
         for xpart, xdefault, xlimits in [
                 ('_pd', 0., pars[name].limits),
@@ -95,6 +95,6 @@
             pars[xname] = Parameter.default(xvalue, name=xname, limits=xlimits)
 
-    pd_types = {}
-    for name in model_info['partype']['pd-2d']:
+    pd_types = {}  # => distribution names
+    for name in model_info['par_type']['pd']:
         xname = name + '_pd_type'
         xvalue = kwargs.pop(xname, 'gaussian')
Index: sasmodels/compare.py
===================================================================
--- sasmodels/compare.py	(revision 72a081dea6caa8fa493057133a6232f6b118c89a)
+++ sasmodels/compare.py	(revision 303d8d66300d33bd04a1db60bb002b734d6f6985)
@@ -308,6 +308,5 @@
         exclude = lambda n: False
     else:
-        partype = model_info['partype']
-        par1d = set(partype['fixed-1d']+partype['pd-1d'])
+        par1d = model_info['par_type']['1d']
         exclude = lambda n: n not in par1d
     lines = []
@@ -870,11 +869,10 @@
         pars, pd_types = bumps_model.create_parameters(model_info, **opts['pars'])
         if not opts['is2d']:
-            active = [base + ext
-                      for base in model_info['partype']['pd-1d']
-                      for ext in ['', '_pd', '_pd_n', '_pd_nsigma']]
-            active.extend(model_info['partype']['fixed-1d'])
-            for k in active:
-                v = pars[k]
-                v.range(*parameter_range(k, v.value))
+            for name in model_info['par_type']['1d']:
+                for ext in ['', '_pd', '_pd_n', '_pd_nsigma']:
+                    k = name+ext
+                    v = pars.get(k, None)
+                    if v is not None:
+                        v.range(*parameter_range(k, v.value))
         else:
             for k, v in pars.items():
Index: sasmodels/core.py
===================================================================
--- sasmodels/core.py	(revision cf52f9cef01302715fe1a7befcf103199a7e4fce)
+++ sasmodels/core.py	(revision 303d8d66300d33bd04a1db60bb002b734d6f6985)
@@ -173,5 +173,5 @@
     return model(q_vectors)
 
-def get_weights(model_info, pars, name):
+def get_weights(parameter, values):
     """
     Generate the distribution for parameter *name* given the parameter values
@@ -181,11 +181,13 @@
     from the *pars* dictionary for parameter value and parameter dispersion.
     """
-    relative = name in model_info['partype']['pd-rel']
-    limits = model_info['limits'][name]
-    disperser = pars.get(name+'_pd_type', 'gaussian')
-    value = pars.get(name, model_info['defaults'][name])
-    npts = pars.get(name+'_pd_n', 0)
-    width = pars.get(name+'_pd', 0.0)
-    nsigma = pars.get(name+'_pd_nsigma', 3.0)
+    value = values.get(parameter.name, parameter.default)
+    if parameter.type not in ('volume', 'orientation'):
+        return [value], []
+    relative = parameter.type == 'volume'
+    limits = parameter.limits
+    disperser = values.get(parameter.name+'_pd_type', 'gaussian')
+    npts = values.get(parameter.name+'_pd_n', 0)
+    width = values.get(parameter.name+'_pd', 0.0)
+    nsigma = values.get(parameter.name+'_pd_nsigma', 3.0)
     value, weight = weights.get_weights(
         disperser, npts, width, nsigma, value, limits, relative)
@@ -206,5 +208,5 @@
     return value, weight
 
-def call_kernel(kernel, pars, cutoff=0, mono=False):
+def call_kernel(kernel, values, cutoff=0, mono=False):
     """
     Call *kernel* returned from :func:`make_kernel` with parameters *pars*.
@@ -219,12 +221,16 @@
     *mono* is True if polydispersity should be set to none on all parameters.
     """
-    fixed_pars = [pars.get(name, kernel.info['defaults'][name])
-                  for name in kernel.fixed_pars]
-    if mono:
-        pd_pars = [( np.array([pars[name]]), np.array([1.0]) )
-                   for name in kernel.pd_pars]
-    else:
-        pd_pars = [get_weights(kernel.info, pars, name) for name in kernel.pd_pars]
-    return kernel(fixed_pars, pd_pars, cutoff=cutoff)
+    if mono or True:
+        pars = np.array([values.get(p.name, p.default)
+                         for p in kernel.info['parameters']])
+        weights = np.array([1.0])
+        details = kernel.info['mono_details']
+        return kernel(pars, weights, details, cutoff)
+    else:
+        pairs = [get_weights(p, values) for p in kernel.info['parameters']]
+        weights, pars = [v for v in zip(*pairs)]
+        details = generate.poly_details(kernel.info, weights, pars)
+        weights, pars = [np.hstack(v) for v in (weights, pars)]
+        return kernel(pars, weights, details, cutoff)
 
 def call_ER_VR(model_info, vol_pars):
@@ -249,16 +255,17 @@
 
 
-def call_ER(info, pars):
-    """
-    Call the model ER function using *pars*.
-    *info* is either *model.info* if you have a loaded model, or *kernel.info*
-    if you have a model kernel prepared for evaluation.
-    """
-    ER = info.get('ER', None)
+def call_ER(model_info, values):
+    """
+    Call the model ER function using *values*. *model_info* is either
+    *model.info* if you have a loaded model, or *kernel.info* if you
+    have a model kernel prepared for evaluation.
+    """
+    ER = model_info.get('ER', None)
     if ER is None:
         return 1.0
     else:
-        vol_pars = [get_weights(info, pars, name)
-                    for name in info['partype']['volume']]
+        vol_pars = [get_weights(parameter, values)
+                    for parameter in model_info['parameters']
+                    if parameter.type == 'volume']
         value, weight = dispersion_mesh(vol_pars)
         individual_radii = ER(*value)
@@ -266,5 +273,5 @@
         return np.sum(weight*individual_radii) / np.sum(weight)
 
-def call_VR(info, pars):
+def call_VR(model_info, values):
     """
     Call the model VR function using *pars*.
@@ -272,10 +279,11 @@
     if you have a model kernel prepared for evaluation.
     """
-    VR = info.get('VR', None)
+    VR = model_info.get('VR', None)
     if VR is None:
         return 1.0
     else:
-        vol_pars = [get_weights(info, pars, name)
-                    for name in info['partype']['volume']]
+        vol_pars = [get_weights(parameter, values)
+                    for parameter in model_info['parameters']
+                    if parameter.type == 'volume']
         value, weight = dispersion_mesh(vol_pars)
         whole, part = VR(*value)
Index: sasmodels/direct_model.py
===================================================================
--- sasmodels/direct_model.py	(revision 02e70ffbd2f37e9a91d5d027972b30069bd05c6b)
+++ sasmodels/direct_model.py	(revision 303d8d66300d33bd04a1db60bb002b734d6f6985)
@@ -66,6 +66,4 @@
             self.data_type = 'Iq'
 
-        partype = model.info['partype']
-
         if self.data_type == 'sesans':
             
@@ -81,4 +79,5 @@
             q_mono = sesans.make_all_q(data)
         elif self.data_type == 'Iqxy':
+            partype = model.info['par_type']
             if not partype['orientation'] and not partype['magnetic']:
                 raise ValueError("not 2D without orientation or magnetic parameters")
Index: sasmodels/generate.py
===================================================================
--- sasmodels/generate.py	(revision 03cac087353e872f40154bf57ef8161eb5367fd4)
+++ sasmodels/generate.py	(revision 303d8d66300d33bd04a1db60bb002b734d6f6985)
@@ -354,5 +354,4 @@
     return [_search(search_path, f) for f in model_info['source']]
 
-
 def convert_type(source, dtype):
     """
@@ -419,6 +418,13 @@
     if filename not in _template_cache or mtime > _template_cache[filename][0]:
         with open(path) as fid:
-            _template_cache[filename] = (mtime, fid.read())
+            _template_cache[filename] = (mtime, fid.read(), path)
     return _template_cache[filename][1]
+
+def model_templates():
+    # TODO: fails DRY; templates are listed in two places.
+    # should instead have model_info contain a list of paths
+    return [joinpath(TEMPLATE_ROOT, filename)
+            for filename in ('kernel_header.c', 'kernel_iq.c')]
+
 
 _FN_TEMPLATE = """\
@@ -450,5 +456,5 @@
     """
     prefix += "."
-    return [prefix+p.name for p in pars]
+    return [prefix+p for p in pars]
 
 _IQXY_PATTERN = re.compile("^((inline|static) )? *(double )? *Iqxy *([(]|$)",
@@ -513,11 +519,11 @@
     # Generate form_volume function, etc. from body only
     if model_info['form_volume'] is not None:
-        pnames = [p.name for p in vol_parameters]
+        pnames = [p for p in vol_parameters]
         source.append(_gen_fn('form_volume', pnames, model_info['form_volume']))
     if model_info['Iq'] is not None:
-        pnames = ['q'] + [p.name for p in iq_parameters]
+        pnames = ['q'] + [p for p in iq_parameters]
         source.append(_gen_fn('Iq', pnames, model_info['Iq']))
     if model_info['Iqxy'] is not None:
-        pnames = ['qx', 'qy'] + [p.name for p in iqxy_parameters]
+        pnames = ['qx', 'qy'] + [p for p in iqxy_parameters]
         source.append(_gen_fn('Iqxy', pnames, model_info['Iqxy']))
 
@@ -552,5 +558,5 @@
 
     # Fill in definitions for numbers of parameters
-    source.append("#define MAX_PD %s"%MAX_PD)
+    source.append("#define MAX_PD %s"%model_info['max_pd'])
     source.append("#define NPARS %d"%(len(model_info['parameters'])-2))
 
@@ -584,16 +590,14 @@
     * *magnetic* set of parameters that are used to compute magnetic
       patterns (which includes all 1D and 2D parameters)
-    * *sesans* set of parameters that are used to compute sesans patterns
-     (which is just 1D without background)
-    * *pd-relative* is the set of parameters with relative distribution
+    * *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).
     """
     par_set = {}
-    par_set['1d'] = [p for p in pars if p.type not in ('orientation', 'magnetic')]
-    par_set['2d'] = [p for p in pars if p.type != 'magnetic']
-    par_set['magnetic'] = [p for p in pars]
-    par_set['pd'] = [p for p in pars if p.type in ('volume', 'orientation')]
-    par_set['pd_relative'] = [p for p in pars if p.type == 'volume']
+    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']
     return par_set
 
@@ -621,5 +625,5 @@
     }
     for p in pars:
-        par_type[p.type if p.type else 'other'].append(p)
+        par_type[p.type if p.type else 'other'].append(p.name)
     return  par_type
 
@@ -641,10 +645,42 @@
     par_type.update(categorize_parameters(pars))
     model_info['parameters'] = pars
-    model_info['limits'] = dict((p.name, p.limits) for p in pars)
     model_info['par_type'] = par_type
-    model_info['defaults'] = dict((p.name, p.default) for p in pars)
     if model_info.get('demo', None) is None:
-        model_info['demo'] = model_info['defaults']
+        model_info['demo'] = dict((p.name, p.default) for p in pars)
     model_info['has_2d'] = par_type['orientation'] or par_type['magnetic']
+
+def mono_details(max_pd, npars):
+    par_offset = 5*max_pd
+    const_offset = par_offset + 3*npars
+
+    mono = np.zeros(const_offset + 3, 'i')
+    mono[0] = 0                # pd_par: arbitrary order; use first
+    mono[1*max_pd] = 1         # pd_length: only one element
+    mono[2*max_pd] = 2         # pd_offset: skip scale and background
+    mono[3*max_pd] = 1         # pd_stride: vectors of length 1
+    mono[4*max_pd-1] = 1       # pd_stride[-1]: only one element in set
+    mono[4*max_pd] = 0         # pd_isvol: doens't matter if no norm
+    mono[par_offset:par_offset+npars] = np.arange(2, npars+2, dtype='i')
+    # par_offset: copied in order
+    mono[par_offset+npars:par_offset+2*npars] = 0
+    # par_coord: no coordinated parameters
+    mono[par_offset+npars] = 1 # par_coord[0]: except par 0
+    mono[par_offset+2*npars:par_offset+3*npars] = 0
+    # fast coord with 0
+    mono[const_offset] = 1     # fast_coord_count: one fast index
+    mono[const_offset+1] = -1  # theta_var: None
+    mono[const_offset+2] = 0   # fast_theta: False
+    return mono
+
+def poly_details(model_info, weights, pars, constraints=None):
+    if constraints is not None:
+        # Need to find the independently varying pars and sort them
+        # Need to build a coordination list for the dependent variables
+        # Need to generate a constraints function which takes values
+        # and weights, returning par blocks
+        raise ValueError("Can't handle constraints yet")
+
+    raise ValueError("polydispersity not supported")
+
 
 def create_default_functions(model_info):
@@ -688,9 +724,5 @@
       model variants (e.g., the list of cases) or is None if there are no
       model variants
-    * *defaults* is the *{parameter: value}* table built from the parameter
-      description table.
-    * *limits* is the *{parameter: [min, max]}* table built from the
-      parameter description table.
-    * *partypes* categorizes the model parameters. See
+    * *par_type* categorizes the model parameters. See
       :func:`categorize_parameters` for details.
     * *demo* contains the *{parameter: value}* map used in compare (and maybe
@@ -709,4 +741,7 @@
       *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*.
 
     """
@@ -719,4 +754,5 @@
         name = " ".join(w.capitalize() for w in kernel_id.split('_'))
     model_info = dict(
+        max_pd=MAX_PD,
         id=kernel_id,  # string used to load the kernel
         filename=abspath(kernel_module.__file__),
@@ -736,4 +772,5 @@
         oldpars=getattr(kernel_module, 'oldpars', {}),
         tests=getattr(kernel_module, 'tests', []),
+        mono_details = mono_details(MAX_PD, len(kernel_module.parameters))
         )
     process_parameters(model_info)
Index: sasmodels/kernel_iq.c
===================================================================
--- sasmodels/kernel_iq.c	(revision 03cac087353e872f40154bf57ef8161eb5367fd4)
+++ sasmodels/kernel_iq.c	(revision 303d8d66300d33bd04a1db60bb002b734d6f6985)
@@ -40,4 +40,6 @@
 void KERNEL_NAME(
     int nq,                 // number of q values
+    const int pd_start,     // where we are in the polydispersity loop
+    const int pd_stop,      // where we are stopping in the polydispersity loop
     global const ProblemDetails *problem,
     global const double *weights,
@@ -45,10 +47,8 @@
     global const double *q, // nq q values, with padding to boundary
     global double *result,  // nq+3 return values, again with padding
-    const double cutoff,    // cutoff in the polydispersity weight product
-    const int pd_start,     // where we are in the polydispersity loop
-    const int pd_stop       // where we are stopping in the polydispersity loop
+    const double cutoff     // cutoff in the polydispersity weight product
     )
 {
-
+printf("hello\n");
   // Storage for the current parameter values.  These will be updated as we
   // walk the polydispersity cube.
@@ -177,4 +177,6 @@
     #endif
 
+    // Accumulate I(q)
+    // Note: weight==0 must always be excluded
     if (weight > cutoff) {
       norm += weight;
Index: sasmodels/kerneldll.py
===================================================================
--- sasmodels/kerneldll.py	(revision 03cac087353e872f40154bf57ef8161eb5367fd4)
+++ sasmodels/kerneldll.py	(revision 303d8d66300d33bd04a1db60bb002b734d6f6985)
@@ -142,5 +142,7 @@
 
     source = generate.convert_type(source, dtype)
-    source_files = generate.model_sources(model_info) + [model_info['filename']]
+    source_files = (generate.model_sources(model_info)
+                    + [model_info['filename']]
+                    + generate.model_templates())
     dll = dll_path(model_info, dtype)
     newest = max(os.path.getmtime(f) for f in source_files)
@@ -172,8 +174,4 @@
     return DllModel(filename, model_info, dtype=dtype)
 
-
-IQ_ARGS = [c_void_p, c_void_p, c_int]
-IQXY_ARGS = [c_void_p, c_void_p, c_void_p, c_int]
-
 class DllModel(object):
     """
@@ -197,9 +195,4 @@
 
     def _load_dll(self):
-        Nfixed1d = len(self.info['partype']['fixed-1d'])
-        Nfixed2d = len(self.info['partype']['fixed-2d'])
-        Npd1d = len(self.info['partype']['pd-1d'])
-        Npd2d = len(self.info['partype']['pd-2d'])
-
         #print("dll", self.dllpath)
         try:
@@ -212,11 +205,11 @@
               else c_double if self.dtype == generate.F64
               else c_longdouble)
-        pd_args_1d = [c_void_p, fp] + [c_int]*Npd1d if Npd1d else []
-        pd_args_2d = [c_void_p, fp] + [c_int]*Npd2d if Npd2d else []
+
+        # int, int, int, int*, double*, double*, double*, double*, double*, double
+        argtypes = [c_int]*3 + [c_void_p]*5 + [fp]
         self.Iq = self.dll[generate.kernel_name(self.info, False)]
-        self.Iq.argtypes = IQ_ARGS + pd_args_1d + [fp]*Nfixed1d
-
         self.Iqxy = self.dll[generate.kernel_name(self.info, True)]
-        self.Iqxy.argtypes = IQXY_ARGS + pd_args_2d + [fp]*Nfixed2d
+        self.Iq.argtypes = argtypes
+        self.Iqxy.argtypes = argtypes
 
     def __getstate__(self):
@@ -263,33 +256,29 @@
         self.q_input = q_input
         self.kernel = kernel
-        self.res = np.empty(q_input.nq, q_input.dtype)
+        self.res = np.empty(q_input.nq+3, q_input.dtype)
         dim = '2d' if q_input.is_2d else '1d'
-        self.fixed_pars = model_info['partype']['fixed-' + dim]
-        self.pd_pars = model_info['partype']['pd-' + dim]
+        self.parameters = model_info['par_type'][dim]
 
         # In dll kernel, but not in opencl kernel
         self.p_res = self.res.ctypes.data
 
-    def __call__(self, fixed_pars, pd_pars, cutoff):
+    def __call__(self, details, values, weights, cutoff):
         real = (np.float32 if self.q_input.dtype == generate.F32
                 else np.float64 if self.q_input.dtype == generate.F64
                 else np.float128)
-
-        nq = c_int(self.q_input.nq)
-        if pd_pars:
-            cutoff = real(cutoff)
-            loops_N = [np.uint32(len(p[0])) for p in pd_pars]
-            loops = np.hstack(pd_pars)
-            loops = np.ascontiguousarray(loops.T, self.q_input.dtype).flatten()
-            p_loops = loops.ctypes.data
-            dispersed = [p_loops, cutoff] + loops_N
-        else:
-            dispersed = []
-        fixed = [real(p) for p in fixed_pars]
-        args = self.q_input.q_pointers + [self.p_res, nq] + dispersed + fixed
-        #print(pars)
+        args = [
+            self.q_input.nq, # nq
+            0, # pd_start
+            1, # pd_stop
+            details.ctypes.data, # problem
+            weights.ctypes.data,  # weights
+            values.ctypes.data,  #pars
+            self.q_input.q_pointers[0], #q
+            self.p_res,   # results
+            real(cutoff), # cutoff
+            ]
         self.kernel(*args)
 
-        return self.res
+        return self.res[:-3]
 
     def release(self):
Index: sasmodels/product.py
===================================================================
--- sasmodels/product.py	(revision 72a081dea6caa8fa493057133a6232f6b118c89a)
+++ sasmodels/product.py	(revision 303d8d66300d33bd04a1db60bb002b734d6f6985)
@@ -99,6 +99,6 @@
         # a parameter map.
         par_map = {}
-        p_info = p_kernel.info['partype']
-        s_info = s_kernel.info['partype']
+        p_info = p_kernel.info['par_type']
+        s_info = s_kernel.info['par_type']
         vol_pars = set(p_info['volume'])
         if dim == '2d':
Index: sasmodels/resolution.py
===================================================================
--- sasmodels/resolution.py	(revision a146eaab015a1387f12891d14d24aa44e466c81c)
+++ sasmodels/resolution.py	(revision 303d8d66300d33bd04a1db60bb002b734d6f6985)
@@ -504,7 +504,7 @@
     from scipy.integrate import romberg
 
-    if any(k not in form.info['defaults'] for k in pars.keys()):
-        keys = set(form.info['defaults'].keys())
-        extra = set(pars.keys()) - keys
+    par_set = set([p.name for p in form.info['parameters']])
+    if any(k not in par_set for k in pars.keys()):
+        extra = set(pars.keys()) - par_set
         raise ValueError("bad parameters: [%s] not in [%s]"%
                          (", ".join(sorted(extra)), ", ".join(sorted(keys))))
@@ -558,7 +558,7 @@
     from scipy.integrate import romberg
 
-    if any(k not in form.info['defaults'] for k in pars.keys()):
-        keys = set(form.info['defaults'].keys())
-        extra = set(pars.keys()) - keys
+    par_set = set([p.name for p in form.info['parameters']])
+    if any(k not in par_set for k in pars.keys()):
+        extra = set(pars.keys()) - par_set
         raise ValueError("bad parameters: [%s] not in [%s]"%
                          (", ".join(sorted(extra)), ", ".join(sorted(keys))))
