Index: doc/genapi.py
===================================================================
--- doc/genapi.py	(revision 3d5c6f859c4a9840b418a6d68e9b567185d00b12)
+++ doc/genapi.py	(revision a5b8477c53695e7df03ecec7346f0c1bd770d3f7)
@@ -59,19 +59,28 @@
     #('alignment', 'GPU data alignment [unused]'),
     ('bumps_model', 'Bumps interface'),
+    ('compare', 'Compare models on different compute engines'),
     ('convert', 'Sasview to sasmodel converter'),
     ('core', 'Model access'),
+    ('data', 'Data layout and plotting routines'),
+    ('details', 'Parameter packing for kernel calls'),
     ('direct_model', 'Simple interface'),
     ('exception', 'Annotate exceptions'),
+    #('frozendict', 'Freeze a dictionary to make it immutable'),
     ('generate', 'Model parser'),
+    ('kernel', 'Evaluator type definitions'),
     ('kernelcl', 'OpenCL model evaluator'),
     ('kerneldll', 'Ctypes model evaluator'),
     ('kernelpy', 'Python model evaluator'),
+    ('list_pars', 'Identify all parameters in all models'),
+    ('mixture', 'Mixture model evaluator'),
     ('model_test', 'Unit test support'),
+    ('modelinfo', 'Parameter and model definitions'),
+    ('product', 'Product model evaluator'),
     ('resolution', '1-D resolution functions'),
     ('resolution2d', '2-D resolution functions'),
     ('sasview_model', 'Sasview interface'),
-    ('sesans', 'SESANS model evaluator'),
+    ('sesans', 'SESANS calculation routines'),
+    #('transition', 'Model stepper for automatic model selection'),
     ('weights', 'Distribution functions'),
-    #('transition', 'Model stepper for automatic model selection'),
 ]
 package='sasmodels'
Index: doc/genmodel.py
===================================================================
--- doc/genmodel.py	(revision 89f41635a25c3421f84f194bd2384d939489b0de)
+++ doc/genmodel.py	(revision a5b8477c53695e7df03ecec7346f0c1bd770d3f7)
@@ -1,6 +1,7 @@
+from __future__ import print_function
+
 import sys, os, math, re
 import numpy as np
 import matplotlib.pyplot as plt
-import pylab
 sys.path.insert(0, os.path.abspath('..'))
 from sasmodels import generate, core
@@ -8,35 +9,18 @@
 from sasmodels.data import empty_data1D, empty_data2D
 
-
-# Convert ../sasmodels/models/name.py to name
-model_name = os.path.basename(sys.argv[1])[:-3]
-model_info = core.load_model_info(model_name)
-model = core.build_model(model_info)
-
-# Load the doc string from the module definition file and store it in rst
-docstr = generate.make_doc(model_info)
-
-
-# Calculate 1D curve for default parameters
-pars = dict((p.name, p.default) for p in model_info['parameters'])
-
-# Plotting ranges and options
-opts = {
-    'xscale'    : 'log',
-    'yscale'    : 'log' if not model_info['structure_factor'] else 'linear',
-    'zscale'    : 'log' if not model_info['structure_factor'] else 'linear',
-    'q_min'     : 0.001,
-    'q_max'     : 1.0,
-    'nq'        : 1000,
-    'nq2d'      : 1000,
-    'vmin'      : 1e-3,  # floor for the 2D data results
-    'qx_max'    : 0.5,
-    #'colormap'  : 'gist_ncar',
-    'colormap'  : 'nipy_spectral',
-    #'colormap'  : 'jet',
-}
-
+try:
+    from typing import Dict, Any
+except ImportError:
+    pass
+else:
+    from matplotlib.axes import Axes
+    from sasmodels.kernel import KernelModel
+    from sasmodels.modelinfo import ModelInfo
 
 def plot_1d(model, opts, ax):
+    # type: (KernelModel, Dict[str, Any], Axes) -> None
+    """
+    Create a 1-D image.
+    """
     q_min, q_max, nq = opts['q_min'], opts['q_max'], opts['nq']
     q_min = math.log10(q_min)
@@ -47,5 +31,5 @@
     Iq1D = calculator()
 
-    ax.plot(q, Iq1D, color='blue', lw=2, label=model_info['name'])
+    ax.plot(q, Iq1D, color='blue', lw=2, label=model.info.name)
     ax.set_xlabel(r'$Q \/(\AA^{-1})$')
     ax.set_ylabel(r'$I(Q) \/(\mathrm{cm}^{-1})$')
@@ -55,6 +39,10 @@
 
 def plot_2d(model, opts, ax):
+    # type: (KernelModel, Dict[str, Any], Axes) -> None
+    """
+    Create a 2-D image.
+    """
     qx_max, nq2d = opts['qx_max'], opts['nq2d']
-    q = np.linspace(-qx_max, qx_max, nq2d)
+    q = np.linspace(-qx_max, qx_max, nq2d) # type: np.ndarray
     data2d = empty_data2D(q, resolution=0.0)
     calculator = DirectModel(data2d, model)
@@ -64,72 +52,123 @@
         Iq2D = np.log(np.clip(Iq2D, opts['vmin'], np.inf))
     ax.imshow(Iq2D, interpolation='nearest', aspect=1, origin='lower',
-        extent=[-qx_max, qx_max, -qx_max, qx_max], cmap=opts['colormap'])
+              extent=[-qx_max, qx_max, -qx_max, qx_max], cmap=opts['colormap'])
     ax.set_xlabel(r'$Q_x \/(\AA^{-1})$')
     ax.set_ylabel(r'$Q_y \/(\AA^{-1})$')
 
-# Generate image 
-fig_height = 3.0 # in
-fig_left = 0.6 # in
-fig_right = 0.5 # in
-fig_top = 0.6*0.25 # in
-fig_bottom = 0.6*0.75
-if model_info['has_2d']:
-    plot_height = fig_height - (fig_top+fig_bottom)
-    plot_width = plot_height
-    fig_width = 2*(plot_width + fig_left + fig_right)
-    aspect = (fig_width, fig_height)
-    ratio = aspect[0]/aspect[1]
-    ax_left = fig_left/fig_width
-    ax_bottom = fig_bottom/fig_height
-    ax_height = plot_height/fig_height
-    ax_width = ax_height/ratio # square axes
-    fig = plt.figure(figsize=aspect)
-    ax2d = fig.add_axes([0.5+ax_left, ax_bottom, ax_width, ax_height])
-    plot_2d(model, opts, ax2d)
-    ax1d = fig.add_axes([ax_left, ax_bottom, ax_width, ax_height])
-    plot_1d(model, opts, ax1d)
-    #ax.set_aspect('square')
-else:
-    plot_height = fig_height - (fig_top+fig_bottom)
-    plot_width = (1+np.sqrt(5))/2*fig_height
-    fig_width = plot_width + fig_left + fig_right
-    ax_left = fig_left/fig_width
-    ax_bottom = fig_bottom/fig_height
-    ax_width = plot_width/fig_width
-    ax_height = plot_height/fig_height
-    aspect = (fig_width, fig_height)
-    fig = plt.figure(figsize=aspect)
-    ax1d = fig.add_axes([ax_left, ax_bottom, ax_width, ax_height])
-    plot_1d(model, opts, ax1d)
+def figfile(model_info):
+    # type: (ModelInfo) -> str
+    return model_info.id + '_autogenfig.png'
 
-# Save image in model/img
-figname = model_name + '_autogenfig.png'
-filename = os.path.join('model', 'img', figname)
-plt.savefig(filename, bbox_inches='tight')
-#print "figure saved in",filename
+def make_figure(model_info, opts):
+    # type: (ModelInfo, Dict[str, Any]) -> None
+    """
+    Generate the figure file to include in the docs.
+    """
+    model = core.build_model(model_info)
 
-# Auto caption for figure
-captionstr = '\n'
-captionstr += '.. figure:: img/' + model_info['id'] + '_autogenfig.png\n'
-captionstr += '\n'
-if model_info['has_2d']:
-    captionstr += '    1D and 2D plots corresponding to the default parameters of the model.\n'
-else:
-    captionstr += '    1D plot corresponding to the default parameters of the model.\n'
-captionstr += '\n'
+    fig_height = 3.0 # in
+    fig_left = 0.6 # in
+    fig_right = 0.5 # in
+    fig_top = 0.6*0.25 # in
+    fig_bottom = 0.6*0.75
+    if model_info.parameters.has_2d:
+        plot_height = fig_height - (fig_top+fig_bottom)
+        plot_width = plot_height
+        fig_width = 2*(plot_width + fig_left + fig_right)
+        aspect = (fig_width, fig_height)
+        ratio = aspect[0]/aspect[1]
+        ax_left = fig_left/fig_width
+        ax_bottom = fig_bottom/fig_height
+        ax_height = plot_height/fig_height
+        ax_width = ax_height/ratio # square axes
+        fig = plt.figure(figsize=aspect)
+        ax2d = fig.add_axes([0.5+ax_left, ax_bottom, ax_width, ax_height])
+        plot_2d(model, opts, ax2d)
+        ax1d = fig.add_axes([ax_left, ax_bottom, ax_width, ax_height])
+        plot_1d(model, opts, ax1d)
+        #ax.set_aspect('square')
+    else:
+        plot_height = fig_height - (fig_top+fig_bottom)
+        plot_width = (1+np.sqrt(5))/2*fig_height
+        fig_width = plot_width + fig_left + fig_right
+        ax_left = fig_left/fig_width
+        ax_bottom = fig_bottom/fig_height
+        ax_width = plot_width/fig_width
+        ax_height = plot_height/fig_height
+        aspect = (fig_width, fig_height)
+        fig = plt.figure(figsize=aspect)
+        ax1d = fig.add_axes([ax_left, ax_bottom, ax_width, ax_height])
+        plot_1d(model, opts, ax1d)
 
-# Add figure reference and caption to documentation (at end, before References)
-pattern = '\*\*REFERENCE'
-m = re.search(pattern, docstr.upper())
+    # Save image in model/img
+    path = os.path.join('model', 'img', figfile(model_info))
+    plt.savefig(path, bbox_inches='tight')
+    #print("figure saved in",path)
 
-if m:
-    docstr1 = docstr[:m.start()]
-    docstr2 = docstr[m.start():]
-    docstr = docstr1 + captionstr + docstr2
-else:
-    print '------------------------------------------------------------------'
-    print 'References NOT FOUND for model: ', model_info['id']
-    print '------------------------------------------------------------------'
-    docstr = docstr + captionstr
+def gen_docs(model_info):
+    # type: (ModelInfo) -> None
+    """
+    Generate the doc string with the figure inserted before the references.
+    """
 
-open(sys.argv[2],'w').write(docstr)
+    # Load the doc string from the module definition file and store it in rst
+    docstr = generate.make_doc(model_info)
+
+    # Auto caption for figure
+    captionstr = '\n'
+    captionstr += '.. figure:: img/' + figfile(model_info) + '\n'
+    captionstr += '\n'
+    if model_info.parameters.has_2d:
+        captionstr += '    1D and 2D plots corresponding to the default parameters of the model.\n'
+    else:
+        captionstr += '    1D plot corresponding to the default parameters of the model.\n'
+    captionstr += '\n'
+
+    # Add figure reference and caption to documentation (at end, before References)
+    pattern = '\*\*REFERENCE'
+    match = re.search(pattern, docstr.upper())
+
+    if match:
+        docstr1 = docstr[:match.start()]
+        docstr2 = docstr[match.start():]
+        docstr = docstr1 + captionstr + docstr2
+    else:
+        print('------------------------------------------------------------------')
+        print('References NOT FOUND for model: ', model_info.id)
+        print('------------------------------------------------------------------')
+        docstr += captionstr
+
+    open(sys.argv[2],'w').write(docstr)
+
+def process_model(path):
+    # type: (str) -> None
+    """
+    Generate doc file and image file for the given model definition file.
+    """
+
+    # Load the model file
+    model_name = os.path.basename(path)[:-3]
+    model_info = core.load_model_info(model_name)
+
+    # Plotting ranges and options
+    opts = {
+        'xscale'    : 'log',
+        'yscale'    : 'log' if not model_info.structure_factor else 'linear',
+        'zscale'    : 'log' if not model_info.structure_factor else 'linear',
+        'q_min'     : 0.001,
+        'q_max'     : 1.0,
+        'nq'        : 1000,
+        'nq2d'      : 1000,
+        'vmin'      : 1e-3,  # floor for the 2D data results
+        'qx_max'    : 0.5,
+        #'colormap'  : 'gist_ncar',
+        'colormap'  : 'nipy_spectral',
+        #'colormap'  : 'jet',
+    }
+
+    # Generate the RST file and the figure.  Order doesn't matter.
+    gen_docs(model_info)
+    make_figure(model_info, opts)
+
+if __name__ == "__main__":
+    process_model(sys.argv[1])
Index: doc/gentoc.py
===================================================================
--- doc/gentoc.py	(revision 5041682338ebf95a462e3296b92e84d6402011ff)
+++ doc/gentoc.py	(revision a5b8477c53695e7df03ecec7346f0c1bd770d3f7)
@@ -9,4 +9,10 @@
 from sasmodels.core import load_model_info
 
+try:
+    from typing import Optional, BinaryIO, List, Dict
+except ImportError:
+    pass
+else:
+    from sasmodels.modelinfo import ModelInfo
 
 TEMPLATE="""\
@@ -27,4 +33,5 @@
 
 def _make_category(category_name, label, title, parent=None):
+    # type: (str, str, str, Optional[BinaryIO]) -> BinaryIO
     file = open(joinpath(MODEL_TOC_PATH, category_name+".rst"), "w")
     file.write(TEMPLATE%{'label':label, 'title':title, 'bar':'*'*len(title)})
@@ -34,10 +41,13 @@
 
 def _add_subcategory(category_name, parent):
+    # type: (str, BinaryIO) -> None
     parent.write("    %s.rst\n"%category_name)
 
 def _add_model(file, model_name):
+    # type: (IO[str], str) -> None
     file.write("    ../../model/%s.rst\n"%model_name)
 
 def _maybe_make_category(category, models, cat_files, model_toc):
+    # type: (str, List[str], Dict[str, BinaryIO], BinaryIO) -> None
     if category not in cat_files:
         print("Unexpected category %s containing"%category, models, file=sys.stderr)
@@ -46,9 +56,10 @@
 
 def generate_toc(model_files):
+    # type: (List[str]) -> None
     if not model_files:
         print("gentoc needs a list of model files", file=sys.stderr)
 
     # find all categories
-    category = {}
+    category = {} # type: Dict[str, List[str]]
     for item in model_files:
         # assume model is in sasmodels/models/name.py, and ignore the full path
@@ -56,8 +67,8 @@
         if model_name.startswith('_'): continue
         model_info = load_model_info(model_name)
-        if model_info['category'] is None:
+        if model_info.category is None:
             print("Missing category for", item, file=sys.stderr)
         else:
-            category.setdefault(model_info['category'],[]).append(model_name)
+            category.setdefault(model_info.category,[]).append(model_name)
 
     # Check category names
Index: sasmodels/data.py
===================================================================
--- sasmodels/data.py	(revision 7ae2b7f85895f1eddd6ef18f37ff501467a34961)
+++ sasmodels/data.py	(revision a5b8477c53695e7df03ecec7346f0c1bd770d3f7)
@@ -37,5 +37,13 @@
 import numpy as np  # type: ignore
 
+try:
+    from typing import Union, Dict, List, Optional
+except ImportError:
+    pass
+else:
+    Data = Union["Data1D", "Data2D", "SesansData"]
+
 def load_data(filename):
+    # type: (str) -> Data
     """
     Load data using a sasview loader.
@@ -50,4 +58,5 @@
 
 def set_beam_stop(data, radius, outer=None):
+    # type: (Data, float, Optional[float]) -> None
     """
     Add a beam stop of the given *radius*.  If *outer*, make an annulus.
@@ -65,4 +74,5 @@
 
 def set_half(data, half):
+    # type: (Data, str) -> None
     """
     Select half of the data, either "right" or "left".
@@ -78,4 +88,5 @@
 
 def set_top(data, cutoff):
+    # type: (Data, float) -> None
     """
     Chop the top off the data, above *cutoff*.
@@ -114,4 +125,5 @@
     """
     def __init__(self, x=None, y=None, dx=None, dy=None):
+        # type: (Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]) -> None
         self.x, self.y, self.dx, self.dy = x, y, dx, dy
         self.dxl = None
@@ -127,4 +139,5 @@
 
     def xaxis(self, label, unit):
+        # type: (str, str) -> None
         """
         set the x axis label and unit
@@ -134,4 +147,5 @@
 
     def yaxis(self, label, unit):
+        # type: (str, str) -> None
         """
         set the y axis label and unit
@@ -140,5 +154,8 @@
         self._yunit = unit
 
-
+class SesansData(Data1D):
+    def __init__(self, **kw):
+        Data1D.__init__(self, **kw)
+        self.lam = None # type: Optional[np.ndarray]
 
 class Data2D(object):
@@ -175,4 +192,5 @@
     """
     def __init__(self, x=None, y=None, z=None, dx=None, dy=None, dz=None):
+        # type: (Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]) -> None
         self.qx_data, self.dqx_data = x, dx
         self.qy_data, self.dqy_data = y, dy
@@ -197,4 +215,5 @@
 
     def xaxis(self, label, unit):
+        # type: (str, str) -> None
         """
         set the x axis label and unit
@@ -204,4 +223,5 @@
 
     def yaxis(self, label, unit):
+        # type: (str, str) -> None
         """
         set the y axis label and unit
@@ -211,4 +231,5 @@
 
     def zaxis(self, label, unit):
+        # type: (str, str) -> None
         """
         set the y axis label and unit
@@ -223,4 +244,5 @@
     """
     def __init__(self, x=None, y=None, z=None):
+        # type: (float, float, Optional[float]) -> None
         self.x, self.y, self.z = x, y, z
 
@@ -230,4 +252,5 @@
     """
     def __init__(self, pixel_size=(None, None), distance=None):
+        # type: (Tuple[float, float], float) -> None
         self.pixel_size = Vector(*pixel_size)
         self.distance = distance
@@ -238,4 +261,5 @@
     """
     def __init__(self):
+        # type: () -> None
         self.wavelength = np.NaN
         self.wavelength_unit = "A"
@@ -243,4 +267,5 @@
 
 def empty_data1D(q, resolution=0.0):
+    # type: (np.ndarray, float) -> Data1D
     """
     Create empty 1D data using the given *q* as the x value.
@@ -259,4 +284,5 @@
 
 def empty_data2D(qx, qy=None, resolution=0.0):
+    # type: (np.ndarray, Optional[np.ndarray], float) -> Data2D
     """
     Create empty 2D data using the given mesh.
@@ -272,5 +298,5 @@
     Qx, Qy = np.meshgrid(qx, qy)
     Qx, Qy = Qx.flatten(), Qy.flatten()
-    Iq = 100 * np.ones_like(Qx)
+    Iq = 100 * np.ones_like(Qx)  # type: np.ndarray
     dIq = np.sqrt(Iq)
     if resolution != 0:
@@ -300,4 +326,5 @@
 
 def plot_data(data, view='log', limits=None):
+    # type: (Data, str, Optional[Tuple[float, float]]) -> None
     """
     Plot data loaded by the sasview loader.
@@ -323,4 +350,5 @@
 def plot_theory(data, theory, resid=None, view='log',
                 use_data=True, limits=None, Iq_calc=None):
+    # type: (Data, Optional[np.ndarray], Optional[np.ndarray], str, bool, Optional[Tuple[float,float]], Optional[np.ndarray]) -> None
     """
     Plot theory calculation.
@@ -337,4 +365,6 @@
     *limits* sets the intensity limits on the plot; if None then the limits
     are inferred from the data.
+
+    *Iq_calc* is the raw theory values without resolution smearing
     """
     if hasattr(data, 'lam'):
@@ -348,4 +378,5 @@
 
 def protect(fn):
+    # type: (Callable) -> Callable
     """
     Decorator to wrap calls in an exception trapper which prints the
@@ -367,4 +398,5 @@
 def _plot_result1D(data, theory, resid, view, use_data,
                    limits=None, Iq_calc=None):
+    # type: (Data1D, Optional[np.ndarray], Optional[np.ndarray], str, bool, Optional[Tuple[float, float]], Optional[np.ndarray]) -> None
     """
     Plot the data and residuals for 1D data.
@@ -444,4 +476,5 @@
 @protect
 def _plot_result_sesans(data, theory, resid, use_data, limits=None):
+    # type: (SesansData, Optional[np.ndarray], Optional[np.ndarray], bool, Optional[Tuple[float, float]]) -> None
     """
     Plot SESANS results.
@@ -454,10 +487,11 @@
 
     if use_data or use_theory:
-        is_tof = np.any(data.lam!=data.lam[0])
+        is_tof = (data.lam != data.lam[0]).any()
         if num_plots > 1:
             plt.subplot(1, num_plots, 1)
         if use_data:
             if is_tof:
-                plt.errorbar(data.x, np.log(data.y)/(data.lam*data.lam), yerr=data.dy/data.y/(data.lam*data.lam))
+                plt.errorbar(data.x, np.log(data.y)/(data.lam*data.lam),
+                             yerr=data.dy/data.y/(data.lam*data.lam))
             else:
                 plt.errorbar(data.x, data.y, yerr=data.dy)
@@ -487,4 +521,5 @@
 @protect
 def _plot_result2D(data, theory, resid, view, use_data, limits=None):
+    # type: (Data2D, Optional[np.ndarray], Optional[np.ndarray], str, bool, Optional[Tuple[float,float]]) -> None
     """
     Plot the data and residuals for 2D data.
@@ -498,4 +533,5 @@
     # Put theory and data on a common colormap scale
     vmin, vmax = np.inf, -np.inf
+    target = None # type: Optional[np.ndarray]
     if use_data:
         target = data.data[~data.mask]
@@ -546,4 +582,5 @@
 @protect
 def _plot_2d_signal(data, signal, vmin=None, vmax=None, view='log'):
+    # type: (Data2D, np.ndarray, Optional[float], Optional[float], str) -> Tuple[float, float]
     """
     Plot the target value for the data.  This could be the data itself,
@@ -587,4 +624,5 @@
 
 def demo():
+    # type: () -> None
     """
     Load and plot a SAS dataset.
Index: sasmodels/direct_model.py
===================================================================
--- sasmodels/direct_model.py	(revision 7ae2b7f85895f1eddd6ef18f37ff501467a34961)
+++ sasmodels/direct_model.py	(revision a5b8477c53695e7df03ecec7346f0c1bd770d3f7)
@@ -32,6 +32,15 @@
 from . import details
 
-
-def call_kernel(kernel, pars, cutoff=0, mono=False):
+try:
+    from typing import Optional, Dict, Tuple
+except ImportError:
+    pass
+else:
+    from .data import Data
+    from .kernel import Kernel, KernelModel
+    from .modelinfo import Parameter, ParameterSet
+
+def call_kernel(kernel, pars, cutoff=0., mono=False):
+    # type: (Kernel, ParameterSet, float, bool) -> np.ndarray
     """
     Call *kernel* returned from *model.make_kernel* with parameters *pars*.
@@ -64,4 +73,5 @@
 
 def get_weights(parameter, values):
+    # type: (Parameter, Dict[str, float]) -> Tuple[np.ndarray, np.ndarray]
     """
     Generate the distribution for parameter *name* given the parameter values
@@ -106,4 +116,5 @@
     """
     def _interpret_data(self, data, model):
+        # type: (Data, KernelModel) -> None
         # pylint: disable=attribute-defined-outside-init
 
@@ -206,4 +217,5 @@
 
     def _set_data(self, Iq, noise=None):
+        # type: (np.ndarray, Optional[float]) -> None
         # pylint: disable=attribute-defined-outside-init
         if noise is not None:
@@ -223,4 +235,5 @@
 
     def _calc_theory(self, pars, cutoff=0.0):
+        # type: (ParameterSet, float) -> np.ndarray
         if self._kernel is None:
             self._kernel = self._model.make_kernel(self._kernel_inputs)
@@ -258,4 +271,5 @@
     """
     def __init__(self, data, model, cutoff=1e-5):
+        # type: (Data, KernelModel, float) -> None
         self.model = model
         self.cutoff = cutoff
@@ -264,7 +278,9 @@
 
     def __call__(self, **pars):
+        # type: (**float) -> np.ndarray
         return self._calc_theory(pars, cutoff=self.cutoff)
 
     def simulate_data(self, noise=None, **pars):
+        # type: (Optional[float], **float) -> None
         """
         Generate simulated data for the model.
@@ -274,4 +290,5 @@
 
 def main():
+    # type: () -> None
     """
     Program to evaluate a particular model at a set of q values.
Index: sasmodels/generate.py
===================================================================
--- sasmodels/generate.py	(revision 81ec7c88c7d9ae513c2e82846c8da52729686bdf)
+++ sasmodels/generate.py	(revision a5b8477c53695e7df03ecec7346f0c1bd770d3f7)
@@ -123,6 +123,6 @@
     polydispersity values for tests.
 
-An *model_info* dictionary is constructed from the kernel meta data and
-returned to the caller.
+A :class:`modelinfo.ModelInfo` structure is constructed from the kernel meta
+data and returned to the caller.
 
 The doc string at the start of the kernel module will be used to
@@ -132,10 +132,4 @@
 file systems are case-sensitive, so only use lower case characters for
 file names and extensions.
-
-
-The function :func:`make` loads the metadata from the module and returns
-the kernel source.  The function :func:`make_doc` extracts the doc string
-and adds the parameter table to the top.  The function :func:`model_sources`
-returns a list of files required by the model.
 
 Code follows the C99 standard with the following extensions and conditions::
@@ -149,4 +143,17 @@
     all double declarations may be converted to half, float, or long double
     FLOAT_SIZE is the number of bytes in the converted variables
+
+:func:`load_kernel_module` loads the model definition file and
+:modelinfo:`make_model_info` parses it. :func:`make_source`
+converts C-based model definitions to C source code, including the
+polydispersity integral.  :func:`model_sources` returns the list of
+source files the model depends on, and :func:`timestamp` returns
+the latest time stamp amongst the source files (so you can check if
+the model needs to be rebuilt).
+
+The function :func:`make_doc` extracts the doc string and adds the
+parameter table to the top.  *make_figure* in *sasmodels/doc/genmodel*
+creates the default figure for the model.  [These two sets of code
+should mignrate into docs.py so docs can be updated in one place].
 """
 from __future__ import print_function
@@ -585,8 +592,10 @@
     Sq_units = "The returned value is a dimensionless structure factor, $S(q)$."
     docs = convert_section_titles_to_boldface(model_info.docs)
+    pars = make_partable(model_info.parameters.COMMON
+                         + model_info.parameters.kernel_parameters)
     subst = dict(id=model_info.id.replace('_', '-'),
                  name=model_info.name,
                  title=model_info.title,
-                 parameters=make_partable(model_info.parameters.kernel_parameters),
+                 parameters=pars,
                  returns=Sq_units if model_info.structure_factor else Iq_units,
                  docs=docs)
Index: sasmodels/kernel.py
===================================================================
--- sasmodels/kernel.py	(revision 04dc6971e45748ea6403588af4f52ee10891af08)
+++ sasmodels/kernel.py	(revision a5b8477c53695e7df03ecec7346f0c1bd770d3f7)
@@ -20,4 +20,5 @@
 class KernelModel(object):
     info = None  # type: ModelInfo
+    dtype = None # type: np.dtype
     def make_kernel(self, q_vectors):
         # type: (List[np.ndarray]) -> "Kernel"
Index: sasmodels/kernelcl.py
===================================================================
--- sasmodels/kernelcl.py	(revision 7ae2b7f85895f1eddd6ef18f37ff501467a34961)
+++ sasmodels/kernelcl.py	(revision a5b8477c53695e7df03ecec7346f0c1bd770d3f7)
@@ -49,4 +49,5 @@
 """
 from __future__ import print_function
+
 import os
 import warnings
@@ -68,4 +69,11 @@
 from . import generate
 from .kernel import KernelModel, Kernel
+
+try:
+    from typing import Tuple, Callable, Any
+    from .modelinfo import ModelInfo
+    from .details import CallDetails
+except ImportError:
+    pass
 
 # The max loops number is limited by the amount of local memory available
@@ -441,12 +449,13 @@
     Call :meth:`release` when done with the kernel instance.
     """
-    def __init__(self, kernel, model_info, q_vectors, dtype):
-        max_pd = model_info.max_pd
-        npars = len(model_info.parameters)-2
-        q_input = GpuInput(q_vectors, dtype)
-        self.dtype = dtype
-        self.dim = '2d' if q_input.is_2d else '1d'
+    def __init__(self, kernel, model_info, q_vectors):
+        # type: (KernelModel, ModelInfo, List[np.ndarray]) -> None
+        max_pd = model_info.parameters.max_pd
+        npars = len(model_info.parameters.kernel_parameters)-2
+        q_input = GpuInput(q_vectors, kernel.dtype)
         self.kernel = kernel
         self.info = model_info
+        self.dtype = kernel.dtype
+        self.dim = '2d' if q_input.is_2d else '1d'
         self.pd_stop_index = 4*max_pd-1
         # plus three for the normalization values
@@ -456,10 +465,10 @@
         # Note: res may be shorter than res_b if global_size != nq
         env = environment()
-        self.queue = env.get_queue(dtype)
+        self.queue = env.get_queue(kernel.dtype)
 
         # details is int32 data, padded to an 8 integer boundary
         size = ((max_pd*5 + npars*3 + 2 + 7)//8)*8
         self.result_b = cl.Buffer(self.queue.context, mf.READ_WRITE,
-                               q_input.global_size[0] * q_input.dtype.itemsize)
+                               q_input.global_size[0] * kernel.dtype.itemsize)
         self.q_input = q_input # allocated by GpuInput above
 
@@ -467,4 +476,5 @@
 
     def __call__(self, call_details, weights, values, cutoff):
+        # type: (CallDetails, np.ndarray, np.ndarray, float) -> np.ndarray
         real = (np.float32 if self.q_input.dtype == generate.F32
                 else np.float64 if self.q_input.dtype == generate.F64
Index: sasmodels/kerneldll.py
===================================================================
--- sasmodels/kerneldll.py	(revision 7ae2b7f85895f1eddd6ef18f37ff501467a34961)
+++ sasmodels/kerneldll.py	(revision a5b8477c53695e7df03ecec7346f0c1bd770d3f7)
@@ -55,5 +55,4 @@
 
 from . import generate
-from . import details
 from .kernel import KernelModel, Kernel
 from .kernelpy import PyInput
@@ -279,18 +278,16 @@
         self.dtype = q_input.dtype
         self.dim = '2d' if q_input.is_2d else '1d'
-        self.result = np.empty(q_input.nq+3, q_input.dtype)
+        self.result = np.empty(q_input.nq+1, q_input.dtype)
+        self.real = (np.float32 if self.q_input.dtype == generate.F32
+                     else np.float64 if self.q_input.dtype == generate.F64
+                     else np.float128)
 
     def __call__(self, call_details, weights, values, cutoff):
         # type: (CallDetails, np.ndarray, np.ndarray, float) -> np.ndarray
-        real = (np.float32 if self.q_input.dtype == generate.F32
-                else np.float64 if self.q_input.dtype == generate.F64
-                else np.float128)
-        assert isinstance(call_details, details.CallDetails)
-        assert weights.dtype == real and values.dtype == real
-
-        start, stop = 0, call_details.total_pd
+
         #print("in kerneldll")
         #print("weights", weights)
         #print("values", values)
+        start, stop = 0, call_details.total_pd
         args = [
             self.q_input.nq, # nq
@@ -302,8 +299,8 @@
             self.q_input.q.ctypes.data, #q
             self.result.ctypes.data,   # results
-            real(cutoff), # cutoff
+            self.real(cutoff), # cutoff
             ]
         self.kernel(*args) # type: ignore
-        return self.result[:-3]
+        return self.result[:-1]
 
     def release(self):
Index: sasmodels/modelinfo.py
===================================================================
--- sasmodels/modelinfo.py	(revision 60f03de6b699388ad10f53569d70661864b486d6)
+++ sasmodels/modelinfo.py	(revision a5b8477c53695e7df03ecec7346f0c1bd770d3f7)
@@ -103,4 +103,5 @@
                 limits = (float(low), float(high))
             except Exception:
+                print("user_limits",user_limits)
                 raise ValueError("invalid limits for %s"%name)
             else:
@@ -278,6 +279,6 @@
     15 degrees).
 
-    In the usual process these values are set by :func:`make_parameter_table`
-    and :func:`parse_parameter` therein.
+    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),
@@ -644,5 +645,4 @@
     info.structure_factor = getattr(kernel_module, 'structure_factor', False)
     info.profile_axes = getattr(kernel_module, 'profile_axes', ['x', 'y'])
-    info.variant_info = getattr(kernel_module, 'variant_info', None)
     info.source = getattr(kernel_module, 'source', [])
     # TODO: check the structure of the tests
@@ -670,67 +670,148 @@
     if the name is in a string.
 
-    The *model_info* structure contains the following fields:
-
-    * *id* is the id of the kernel
-    * *name* is the display name of the kernel
-    * *filename* is the full path to the module defining the file (if any)
-    * *title* is a short description of the kernel
-    * *description* is a long description of the kernel (this doesn't seem
-      very useful since the Help button on the model page brings you directly
-      to the documentation page)
-    * *docs* is the docstring from the module.  Use :func:`make_doc` to
-    * *category* specifies the model location in the docs
-    * *parameters* is the model parameter table
-    * *single* is True if the model allows single precision
-    * *structure_factor* is True if the model is useable in a product
-    * *variant_info* contains the information required to select between
-      model variants (e.g., the list of cases) or is None if there are no
-      model variants
-    * *par_type* categorizes the model parameters. See
-      :func:`categorize_parameters` for details.
-    * *demo* contains the *{parameter: value}* map used in compare (and maybe
-      for the demo plot, if plots aren't set up to use the default values).
-      If *demo* is not given in the file, then the default values will be used.
-    * *tests* is a set of tests that must pass
-    * *source* is the list of library files to include in the C model build
-    * *Iq*, *Iqxy*, *form_volume*, *ER*, *VR* and *sesans* are python functions
-      implementing the kernel for the module, or None if they are not
-      defined in python
-    * *composition* is None if the model is independent, otherwise it is a
-      tuple with composition type ('product' or 'mixture') and a list of
-      *model_info* blocks for the composition objects.  This allows us to
-      build complete product and mixture models from just the info.
-    * *control* is the name of the control parameter if there is one.
-    * *hidden* returns the list of hidden parameters given the value of the
-      control parameter
-
     The structure should be mostly static, other than the delayed definition
     of *Iq* and *Iqxy* if they need to be defined.
     """
+    #: Full path to the file defining the kernel, if any.
+    filename = None         # type: Optiona[str]
+    #: Id of the kernel used to load it from the filesystem.
     id = None               # type: str
-    filename = None         # type: str
+    #: Display name of the model, which defaults to the model id but with
+    #: capitalization of the parts so for example core_shell defaults to
+    #: "Core Shell".
     name = None             # type: str
+    #: Short description of the model.
     title = None            # type: str
+    #: Long description of the model.
     description = None      # type: str
+    #: Model parameter table. Parameters are defined using a list of parameter
+    #: definitions, each of which is contains parameter name, units,
+    #: default value, limits, type and description.  See :class:`Parameter`
+    #: for details on the individual parameters.  The parameters are gathered
+    #: into a :class:`ParameterTable`, which provides various views into the
+    #: parameter list.
     parameters = None       # type: ParameterTable
+    #: Demo parameters as a *parameter:value* map used as the default values
+    #: for :mod:`compare`.  Any parameters not set in *demo* will use the
+    #: defaults from the parameter table.  That means no polydispersity, and
+    #: in the case of multiplicity models, a minimal model with no interesting
+    #: scattering.
     demo = None             # type: Dict[str, float]
+    #: Composition is None if this is an independent model, or it is a
+    #: tuple with comoposition type ('product' or 'misture') and a list of
+    #: :class:`ModelInfo` blocks for the composed objects.  This allows us
+    #: to rebuild a complete mixture or product model from the info block.
+    #: *composition* is not given in the model definition file, but instead
+    #: arises when the model is constructed using names such as
+    #: *sphere*hardsphere* or *cylinder+sphere*.
     composition = None      # type: Optional[Tuple[str, List[ModelInfo]]]
+    #: Name of the control parameter for a variant model such as :ref:`rpa`.
+    #: The *control* parameter should appear in the parameter table, with
+    #: limits defined as *[CASES]*, for case names such as
+    #: *CASES = ["diblock copolymer", "triblock copolymer", ...]*.
+    #: This should give *limits=[[case1, case2, ...]]*, but the
+    #: model loader translates this to *limits=[0, len(CASES)-1]*, and adds
+    #: *choices=CASES* to the :class:`Parameter` definition. Note that
+    #: models can use a list of cases as a parameter without it being a
+    #: control parameter.  Either way, the parameter is sent to the model
+    #: evaluator as *float(choice_num)*, where choices are numbered from 0.
+    #: See also :attr:`hidden`.
     control = None          # type: str
+    #: Different variants require different parameters.  In order to show
+    #: just the parameters needed for the variant selected by :attr:`control`,
+    #: you should provide a function *hidden(control) -> set(['a', 'b', ...])*
+    #: indicating which parameters need to be hidden.  For multiplicity
+    #: models, you need to use the complete name of the parameter, including
+    #: its number.  So for example, if variant "a" uses only *sld1* and *sld2*,
+    #: then *sld3*, *sld4* and *sld5* of multiplicity parameter *sld[5]*
+    #: should be in the hidden set.
+    hidden = None           # type: Optional[Callable[[int], Set[str]]]
+    #: Doc string from the top of the model file.  This should be formatted
+    #: using ReStructuredText format, with latex markup in ".. math"
+    #: environments, or in dollar signs.  This will be automatically
+    #: extracted to a .rst file by :func:`generate.make_docs`, then
+    #: converted to HTML or PDF by Sphinx.
     docs = None             # type: str
+    #: Location of the model description in the documentation.  This takes the
+    #: form of "section" or "section:subsection".  So for example,
+    #: :ref:`porod` uses *category="shape-independent"* so it is in the
+    #: :ref:`Shape-independent` section whereas
+    #: :ref:`capped_cylinder` uses: *category="shape:cylinder"*, which puts
+    #: it in the :ref:`shape-cylinder` section.
     category = None         # type: Optional[str]
+    #: True if the model can be computed accurately with single precision.
+    #: This is True by default, but models such as :ref:`bcc_paracrystal` set
+    #: it to False because they require double precision calculations.
     single = None           # type: bool
+    #: True if the model is a structure factor used to model the interaction
+    #: between form factor models.  This will default to False if it is not
+    #: provided in the file.
     structure_factor = None # type: bool
+    #: List of C source files used to define the model.  The source files
+    #: should define the *Iq* function, and possibly *Iqxy*, though a default
+    #: *Iqxy = Iq(sqrt(qx**2+qy**2)* will be created if no *Iqxy* is provided.
+    #: Files containing the most basic functions must appear first in the list,
+    #: followed by the files that use those functions.  Form factors are
+    #: indicated by providing a :attr:`ER` function.
+    source = None           # type: List[str]
+    #: The set of tests that must pass.  The format of the tests is described
+    #: in :mod:`model_test`.
+    tests = None            # type: List[TestCondition]
+    #: Returns the effective radius of the model given its volume parameters.
+    #: The presence of *ER* indicates that the model is a form factor model
+    #: that may be used together with a structure factor to form an implicit
+    #: multiplication model.
+    #:
+    #: The parameters to the *ER* function must be marked with type *volume*.
+    #: in the parameter table.  They will appear in the same order as they
+    #: do in the table.  The values passed to *ER* will be vectors, with one
+    #: value for each polydispersity condition.  For example, if the model
+    #: is polydisperse over both length and radius, then both length and
+    #: radius will have the same number of values in the vector, with one
+    #: value for each *length X radius*.  If only *radius* is polydisperse,
+    #: then the value for *length* will be repeated once for each value of
+    #: *radius*.  The *ER* function should return one effective radius for
+    #: each parameter set.  Multiplicity parameters will be received as
+    #: arrays, with one row per polydispersity condition.
+    ER = None               # type: Optional[Callable[[np.ndarray], np.ndarray]]
+    #: Returns the occupied volume and the total volume for each parameter set.
+    #: See :attr:`ER` for details on the parameters.
+    VR = None               # type: Optional[Callable[[np.ndarray], Tuple[np.ndarray, np.ndarray]]]
+    #: Returns the form volume for python-based models.  Form volume is needed
+    #: for volume normalization in the polydispersity integral.  If no
+    #: parameters are *volume* parameters, then form volume is not needed.
+    #: For C-based models, (with :attr:`sources` defined, or with :attr:`Iq`
+    #: defined using a string containing C code), form_volume must also be
+    #: C code, either defined as a string, or in the sources.
+    form_volume = None      # type: Union[None, str, Callable[[np.ndarray], float]]
+    #: Returns *I(q, a, b, ...)* for parameters *a*, *b*, etc. defined
+    #: by the parameter table.  *Iq* can be defined as a python function, or
+    #: as a C function.  If it is defined in C, then set *Iq* to the body of
+    #: the C function, including the return statement.  This function takes
+    #: values for *q* and each of the parameters as separate *double* values
+    #: (which may be converted to float or long double by sasmodels).  All
+    #: source code files listed in :attr:`sources` will be loaded before the
+    #: *Iq* function is defined.  If *Iq* is not present, then sources should
+    #: define *static double Iq(double q, double a, double b, ...)* which
+    #: will return *I(q, a, b, ...)*.  Multiplicity parameters are sent as
+    #: pointers to doubles.  Constants in floating point expressions should
+    #: include the decimal point. See :mod:`generate` for more details.
+    Iq = None               # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
+    #: Returns *I(qx, qy, a, b, ...)*.  The interface follows :attr:`Iq`.
+    Iqxy = None             # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
+    #: Returns a model profile curve *x, y*.  If *profile* is defined, this
+    #: curve will appear in response to the *Show* button in SasView.  Use
+    #: :attr:`profile_axes` to set the axis labels.  Note that *y* values
+    #: will be scaled by 1e6 before plotting.
+    profile = None          # type: Optional[Callable[[np.ndarray], None]]
+    #: Axis labels for the :attr:`profile` plot.  The default is *['x', 'y']*.
+    #: Only the *x* component is used for now.
     profile_axes = None     # type: Tuple[str, str]
-    variant_info = None     # type: Optional[List[str]]
-    source = None           # type: List[str]
-    tests = None            # type: List[TestCondition]
-    ER = None               # type: Optional[Callable[[np.ndarray], np.ndarray]]
-    VR = None               # type: Optional[Callable[[np.ndarray], Tuple[np.ndarray, np.ndarray]]]
-    form_volume = None      # type: Union[None, str, Callable[[np.ndarray], float]]
-    Iq = None               # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
-    Iqxy = None             # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
-    profile = None          # type: Optional[Callable[[np.ndarray], None]]
+    #: Returns *sesans(z, a, b, ...)* for models which can directly compute
+    #: the SESANS correlation function.  Note: not currently implemented.
     sesans = None           # type: Optional[Callable[[np.ndarray], np.ndarray]]
-    hidden = None           # type: Optional[Callable[[int], Set[str]]]
+    #: :class:details.CallDetails data for mono-disperse function evaluation.
+    #: This field is created automatically by the model loader, and should
+    #: not be defined as part of the model definition file.
     mono_details = None     # type: CallDetails
 
@@ -740,4 +821,10 @@
 
     def get_hidden_parameters(self, control):
+        """
+        Returns the set of hidden parameters for the model.  *control* is the
+        value of the control parameter.  Note that multiplicity models have
+        an implicit control parameter, which is the parameter that controls
+        the multiplicity.
+        """
         if self.hidden is not None:
             hidden = self.hidden(control)
Index: sasmodels/models/rpa.py
===================================================================
--- sasmodels/models/rpa.py	(revision ec45c4f5ff6e17014d968da6eb80308c2c8791f0)
+++ sasmodels/models/rpa.py	(revision a5b8477c53695e7df03ecec7346f0c1bd770d3f7)
@@ -86,5 +86,5 @@
 #   ["name", "units", default, [lower, upper], "type","description"],
 parameters = [
-    ["case_num", "", 1, CASES, "", "Component organization"],
+    ["case_num", "", 1, [CASES], "", "Component organization"],
 
     ["N[4]", "", 1000.0, [1, inf], "", "Degree of polymerization"],
