Index: sasmodels/generate.py
===================================================================
--- sasmodels/generate.py	(revision e6408d049d3598925e345d8adec1cdb0bb0c6adb)
+++ sasmodels/generate.py	(revision a936688e111fcaa3a61f8f8fbd39ea3c38aee6b2)
@@ -679,7 +679,4 @@
     * *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
     * *defaults* is the *{parameter: value}* table built from the parameter
       description table.
@@ -721,5 +718,5 @@
         single=getattr(kernel_module, 'single', True),
         structure_factor=getattr(kernel_module, 'structure_factor', False),
-        variant_info=getattr(kernel_module, 'invariant_info', None),
+        control=getattr(kernel_module, 'control', None),
         demo=getattr(kernel_module, 'demo', None),
         source=getattr(kernel_module, 'source', []),
Index: sasmodels/sasview_model.py
===================================================================
--- sasmodels/sasview_model.py	(revision 4d767110d389aa65268b4b929ac8df1413460380)
+++ sasmodels/sasview_model.py	(revision a936688e111fcaa3a61f8f8fbd39ea3c38aee6b2)
@@ -26,4 +26,21 @@
 from . import custom
 from . import generate
+
+try:
+    from typing import Dict, Mapping, Any, Sequence, Tuple, NamedTuple, List, Optional
+    from .kernel import KernelModel
+    MultiplicityInfoType = NamedTuple(
+        'MuliplicityInfo',
+        [("number", int), ("control", str), ("choices", List[str]),
+         ("x_axis_label", str)])
+except ImportError:
+    pass
+
+# TODO: separate x_axis_label from multiplicity info
+# The x-axis label belongs with the profile generating function
+MultiplicityInfo = collections.namedtuple(
+    'MultiplicityInfo',
+    ["number", "control", "choices", "x_axis_label"],
+)
 
 def load_standard_models():
@@ -69,10 +86,53 @@
     Convert *model_info* into a SasView model wrapper.
     """
-    def __init__(self, multfactor=1):
-        SasviewModel.__init__(self)
-    attrs = dict(__init__=__init__, _model_info=model_info)
+    model_info['variant_info'] = None  # temporary hack for older sasview
+    def __init__(self, multiplicity=1):
+        SasviewModel.__init__(self, multiplicity=multiplicity)
+    attrs = _generate_model_attributes(model_info)
+    attrs['__init__'] = __init__
     ConstructedModel = type(model_info['name'], (SasviewModel,), attrs)
     return ConstructedModel
 
+def _generate_model_attributes(model_info):
+    # type: (ModelInfo) -> Dict[str, Any]
+    """
+    Generate the class attributes for the model.
+
+    This should include all the information necessary to query the model
+    details so that you do not need to instantiate a model to query it.
+
+    All the attributes should be immutable to avoid accidents.
+    """
+    attrs = {}  # type: Dict[str, Any]
+    attrs['_model_info'] = model_info
+    attrs['name'] = model_info['name']
+    attrs['description'] = model_info['description']
+    attrs['category'] = model_info['category']
+
+    # TODO: allow model to override axis labels input/output name/unit
+
+    #self.is_multifunc = False
+    non_fittable = []  # type: List[str]
+    variants = MultiplicityInfo(0, "", [], "")
+    attrs['is_structure_factor'] = model_info['structure_factor']
+    attrs['is_form_factor'] = model_info['ER'] is not None
+    attrs['is_multiplicity_model'] = variants[0] > 1
+    attrs['multiplicity_info'] = variants
+
+    partype = model_info['partype']
+    orientation_params = (
+            partype['orientation']
+            + [n + '.width' for n in partype['orientation']]
+            + partype['magnetic'])
+    magnetic_params = partype['magnetic']
+    fixed = [n + '.width' for n in partype['pd-2d']]
+
+    attrs['orientation_params'] = tuple(orientation_params)
+    attrs['magnetic_params'] = tuple(magnetic_params)
+    attrs['fixed'] = tuple(fixed)
+
+    attrs['non_fittable'] = tuple(non_fittable)
+
+    return attrs
 
 class SasviewModel(object):
@@ -80,27 +140,80 @@
     Sasview wrapper for opencl/ctypes model.
     """
-    _model_info = {}
-    def __init__(self):
+    # Model parameters for the specific model are set in the class constructor
+    # via the _generate_model_attributes function, which subclasses
+    # SasviewModel.  They are included here for typing and documentation
+    # purposes.
+    _model = None       # type: KernelModel
+    _model_info = None  # type: ModelInfo
+    #: load/save name for the model
+    id = None           # type: str
+    #: display name for the model
+    name = None         # type: str
+    #: short model description
+    description = None  # type: str
+    #: default model category
+    category = None     # type: str
+
+    #: names of the orientation parameters in the order they appear
+    orientation_params = None # type: Sequence[str]
+    #: names of the magnetic parameters in the order they appear
+    magnetic_params = None    # type: Sequence[str]
+    #: names of the fittable parameters
+    fixed = None              # type: Sequence[str]
+    # TODO: the attribute fixed is ill-named
+
+    # Axis labels
+    input_name = "Q"
+    input_unit = "A^{-1}"
+    output_name = "Intensity"
+    output_unit = "cm^{-1}"
+
+    #: default cutoff for polydispersity
+    cutoff = 1e-5
+
+    # Note: Use non-mutable values for class attributes to avoid errors
+    #: parameters that are not fitted
+    non_fittable = ()        # type: Sequence[str]
+
+    #: True if model should appear as a structure factor
+    is_structure_factor = False
+    #: True if model should appear as a form factor
+    is_form_factor = False
+    #: True if model has multiplicity
+    is_multiplicity_model = False
+    #: Mulitplicity information
+    multiplicity_info = None # type: MultiplicityInfoType
+
+    # Per-instance variables
+    #: parameter {name: value} mapping
+    params = None      # type: Dict[str, float]
+    #: values for dispersion width, npts, nsigmas and type
+    dispersion = None  # type: Dict[str, Any]
+    #: units and limits for each parameter
+    details = None     # type: Mapping[str, Tuple(str, float, float)]
+    #: multiplicity used, or None if no multiplicity controls
+    multiplicity = None     # type: Optional[int]
+
+    def __init__(self, multiplicity):
+        # type: () -> None
+        print("initializing", self.name)
+        #raise Exception("first initialization")
         self._model = None
-        model_info = self._model_info
-
-        self.name = model_info['name']
-        self.description = model_info['description']
-        self.category = None
-        self.multiplicity_info = None
-        self.is_multifunc = False
-
-        ## interpret the parameters
-        ## TODO: reorganize parameter handling
-        self.details = dict()
+
+        ## _persistency_dict is used by sas.perspectives.fitting.basepage
+        ## to store dispersity reference.
+        self._persistency_dict = {}
+
+        self.multiplicity = multiplicity
+
         self.params = collections.OrderedDict()
-        self.dispersion = dict()
-        partype = model_info['partype']
-
-        for p in model_info['parameters']:
+        self.dispersion = {}
+        self.details = {}
+
+        for p in self._model_info['parameters']:
             self.params[p.name] = p.default
             self.details[p.name] = [p.units] + p.limits
 
-        for name in partype['pd-2d']:
+        for name in self._model_info['partype']['pd-2d']:
             self.dispersion[name] = {
                 'width': 0,
@@ -109,26 +222,4 @@
                 'type': 'gaussian',
             }
-
-        self.orientation_params = (
-            partype['orientation']
-            + [n + '.width' for n in partype['orientation']]
-            + partype['magnetic'])
-        self.magnetic_params = partype['magnetic']
-        self.fixed = [n + '.width' for n in partype['pd-2d']]
-        self.non_fittable = []
-
-        ## independent parameter name and unit [string]
-        self.input_name = model_info.get("input_name", "Q")
-        self.input_unit = model_info.get("input_unit", "A^{-1}")
-        self.output_name = model_info.get("output_name", "Intensity")
-        self.output_unit = model_info.get("output_unit", "cm^{-1}")
-
-        ## _persistency_dict is used by sas.perspectives.fitting.basepage
-        ## to store dispersity reference.
-        ## TODO: _persistency_dict to persistency_dict throughout sasview
-        self._persistency_dict = {}
-
-        ## New fields introduced for opencl rewrite
-        self.cutoff = 1e-5
 
     def __get_state__(self):
Index: sasmodels/weights.py
===================================================================
--- sasmodels/weights.py	(revision 5c962dff13d0a0a2f6d779be8df775f540958dbc)
+++ sasmodels/weights.py	(revision a936688e111fcaa3a61f8f8fbd39ea3c38aee6b2)
@@ -174,5 +174,5 @@
 
 # dispersion name -> disperser lookup table.
-models = dict((d.type, d) for d in (
+MODELS = dict((d.type, d) for d in (
     GaussianDispersion, RectangleDispersion,
     ArrayDispersion, SchulzDispersion, LogNormalDispersion
@@ -201,5 +201,5 @@
     Returns *(value, weight)*, where *value* and *weight* are vectors.
     """
-    cls = models[disperser]
+    cls = MODELS[disperser]
     obj = cls(n, width, nsigmas)
     v, w = obj.get_weights(value, limits[0], limits[1], relative)
@@ -207,5 +207,5 @@
 
 # Hack to allow sasview dispersion objects to interoperate with sasmodels
-dispersers = dict((v.__name__, k) for k, v in models.items())
+dispersers = dict((v.__name__, k) for k, v in MODELS.items())
 dispersers['DispersionModel'] = RectangleDispersion.type
 
