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
