Index: sasmodels/modelinfo.py
===================================================================
--- sasmodels/modelinfo.py	(revision 4bfd2776ad7d85e00cad95f2a94c6553b27a3e7c)
+++ sasmodels/modelinfo.py	(revision a86f6c032464486a45a9cc70a5dbfb51ac55a8b8)
@@ -5,4 +5,20 @@
 
 from .details import mono_details
+
+# Optional typing
+try:
+    from typing import Tuple, List, Union, Dict, Optional, Any, Callable
+except ImportError:
+    pass
+else:
+    from .details import CallDetails
+    Limits = Tuple[float, float]
+    LimitsOrChoice = Union[Limits, Tuple[str]]
+    ParameterDef = Tuple[str, str, float, LimitsOrChoice, str, str]
+    ParameterSetUser = Dict[str, Union[float, List[float]]]
+    ParameterSet = Dict[str, float]
+    TestInput = Union[str, float, List[float], Tuple[float, float], List[Tuple[float, float]]]
+    TestValue = Union[float, List[float]]
+    TestCondition = Tuple[ParameterSetUser, TestInput, TestValue]
 
 MAX_PD = 4
@@ -25,8 +41,10 @@
 # depends on the some polydisperse parameter with the current implementation
 
+
 def make_parameter_table(pars):
+    # type: (List[ParameterDefinition) -> ParameterTable
     processed = []
     for p in pars:
-        if not isinstance(p, list) or len(p) != 6:
+        if not isinstance(p, (list, tuple)) or len(p) != 6:
             raise ValueError("Parameter should be [name, units, default, limits, type, desc], but got %r"
                              %str(p))
@@ -35,6 +53,7 @@
     return partable
 
-def parse_parameter(name, units='', default=None,
-                    limits=(-np.inf, np.inf), type='', description=''):
+def parse_parameter(name, units='', default=np.NaN,
+                    limits=(-np.inf, np.inf), ptype='', description=''):
+    # type: (str, str, float, LimitsOrChoice, str, str) -> Parameter
     # Parameter is a user facing class.  Do robust type checking.
     if not isstr(name):
@@ -68,6 +87,6 @@
                          % (default, name))
 
-    if type not in ("volume", "orientation", "sld", "magnetic", ""):
-        raise ValueError("unexpected type %r for %s" % (type, name))
+    if ptype not in ("volume", "orientation", "sld", "magnetic", ""):
+        raise ValueError("unexpected type %r for %s" % (ptype, name))
 
     if not isstr(description):
@@ -86,6 +105,6 @@
 
     # automatically identify sld types
-    if type=='' and (pid.startswith('sld') or pid.endswith('sld')):
-        type = 'sld'
+    if ptype== '' and (pid.startswith('sld') or pid.endswith('sld')):
+        ptype = 'sld'
 
     # Check if using a vector definition, name[k], as the parameter name
@@ -96,5 +115,5 @@
             length = int(ref)
             control = None
-        except Exception:
+        except ValueError:
             length = None
             control = ref
@@ -105,9 +124,9 @@
     # Build the parameter
     parameter = Parameter(name=name, units=units, default=default,
-                          limits=limits, type=type, description=description)
+                          limits=limits, ptype=ptype, 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.polydisperse = ptype in ('orientation', 'volume')
+    parameter.relative_pd = ptype == 'volume'
     parameter.choices = choices
     parameter.length = length
@@ -118,4 +137,5 @@
 
 def expand_pars(partable, pars):
+    # type: (ParameterTable, ParameterSetUser) ->  ParameterSet
     """
     Create demo parameter set from key-value pairs.
@@ -159,4 +179,5 @@
 
 def prefix_parameter(par, prefix):
+    # type: (Parameter, str) -> Parameter
     """
     Return a copy of the parameter with its name prefixed.
@@ -167,4 +188,5 @@
 
 def suffix_parameter(par, suffix):
+    # type: (Parameter, str) -> Parameter
     """
     Return a copy of the parameter with its name prefixed.
@@ -233,27 +255,29 @@
     """
     def __init__(self, name, units='', default=None, limits=(-np.inf, np.inf),
-                 type='', description=''):
-        self.id = name.split('[')[0].strip()
-        self.name = name
-        self.units = units
-        self.default = default
-        self.limits = limits
-        self.type = type
-        self.description = description
+                 ptype='', description=''):
+        # type: (str, str, float, Limits, str, str)
+        self.id = name.split('[')[0].strip() # type: str
+        self.name = name                     # type: str
+        self.units = units                   # type: str
+        self.default = default               # type: float
+        self.limits = limits                 # type: Limits
+        self.type = ptype                    # type: str
+        self.description = description       # type: str
 
         # Length and length_control will be filled in once the complete
         # parameter table is available.
-        self.length = 1
-        self.length_control = None
-        self.is_control = False
+        self.length = 1                      # type: int
+        self.length_control = None           # type: Optional[str]
+        self.is_control = False              # type: bool
 
         # TODO: need better control over whether a parameter is polydisperse
-        self.polydisperse = False
-        self.relative_pd = False
+        self.polydisperse = False            # type: bool
+        self.relative_pd = False             # type: bool
 
         # choices are also set externally.
-        self.choices = []
+        self.choices = []                    # type: List[str]
 
     def as_definition(self):
+        # type: () -> str
         """
         Declare space for the variable in a parameter structure.
@@ -269,4 +293,5 @@
 
     def as_function_argument(self):
+        # type: () -> str
         """
         Declare the variable as a function argument.
@@ -282,4 +307,5 @@
 
     def as_call_reference(self, prefix=""):
+        # type: () -> str
         # 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.
@@ -287,7 +313,9 @@
 
     def __str__(self):
+        # type: () -> str
         return "<%s>"%self.name
 
     def __repr__(self):
+        # type: () -> str
         return "P<%s>"%self.name
 
@@ -355,12 +383,63 @@
 
     def __init__(self, parameters):
+        # type: (List[Parameter]) -> None
         self.kernel_parameters = parameters
         self._set_vector_lengths()
-        self._make_call_parameter_list()
-        self._categorize_parameters()
-        self._set_defaults()
+        self.call_parameters = self._get_call_parameters()
+        self.defaults = self._get_defaults()
         #self._name_table= dict((p.id, p) for p in parameters)
 
+        # Set the kernel parameters.  Assumes background and scale are the
+        # first two parameters in the parameter list, but these are not sent
+        # to the underlying kernel functions.
+        self.iq_parameters = [p for p in self.kernel_parameters
+                              if p.type not in ('orientation', 'magnetic')]
+        self.iqxy_parameters = [p for p in self.kernel_parameters
+                                if p.type != 'magnetic']
+        self.form_volume_parameters = [p for p in self.kernel_parameters
+                                       if p.type == 'volume']
+
+        # Theta offset
+        offset = 0
+        for p in self.kernel_parameters:
+            if p.name == 'theta':
+                self.theta_offset = offset
+                break
+            offset += p.length
+        else:
+            self.theta_offset = -1
+
+        # number of polydisperse parameters
+        num_pd = sum(p.length for p in self.kernel_parameters if p.polydisperse)
+        # 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.
+        self.max_pd = min(num_pd, MAX_PD)
+
+        self.npars = sum(p.length for p in self.kernel_parameters)
+
+        # true if has 2D parameters
+        self.has_2d = any(p.type in ('orientation', 'magnetic')
+                          for p in self.kernel_parameters)
+
+        self.pd_1d = set(p.name for p in self.call_parameters
+                         if p.polydisperse and p.type not in ('orientation', 'magnetic'))
+        self.pd_2d = set(p.name for p in self.call_parameters
+                         if p.polydisperse and p.type != 'magnetic')
+
+
     def _set_vector_lengths(self):
+        # type: () -> None
+        """
+        Walk the list of kernel parameters, setting the length field of the
+        vector parameters from the upper limit of the reference parameter.
+
+        This needs to be done once the entire parameter table is available
+        since the reference may still be undefined when the parameter is
+        initially created.
+
+        Note: This modifies the underlying parameter object.
+        """
         # Sort out the length of the vector parameters such as thickness[n]
         for p in self.kernel_parameters:
@@ -374,10 +453,19 @@
                 ref.is_control = True
                 low, high = ref.limits
-                if int(low) != low or int(high) != high or low<0 or high>20:
+                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)
+                # TODO: may want to make a copy of the parameter before updating
+                # this introduces other potential problems, since the same
+                # parameter may be referenced elsewhere
                 p.length = high
 
-    def _set_defaults(self):
+    def _get_defaults(self):
+        # type: () -> ParameterSet
+        """
+        Get a list of parameter defaults from the parameters.
+
+        Expands vector parameters into parameter id+number.
+        """
         # Construct default values, including vector defaults
         defaults = {}
@@ -388,7 +476,8 @@
                 for k in range(1, p.length+1):
                     defaults["%s%d"%(p.id, k)] = p.default
-        self.defaults = defaults
-
-    def _make_call_parameter_list(self):
+        return defaults
+
+    def _get_call_parameters(self):
+        # type: () -> List[Parameter]
         full_list = self.COMMON[:]
         for p in self.kernel_parameters:
@@ -402,47 +491,8 @@
                     pk.relative_pd = p.relative_pd
                     full_list.append(pk)
-        self.call_parameters = full_list
-
-    def _categorize_parameters(self):
-        # Set the kernel parameters.  Assumes background and scale are the
-        # first two parameters in the parameter list, but these are not sent
-        # to the underlying kernel functions.
-        self.iq_parameters = [p for p in self.kernel_parameters
-                              if p.type not in ('orientation', 'magnetic')]
-        self.iqxy_parameters = [p for p in self.kernel_parameters
-                                if p.type != 'magnetic']
-        self.form_volume_parameters = [p for p in self.kernel_parameters
-                                       if p.type == 'volume']
-
-        # Theta offset
-        offset = 0
-        for p in self.kernel_parameters:
-            if p.name == 'theta':
-                self.theta_offset = offset
-                break
-            offset += p.length
-        else:
-            self.theta_offset = -1
-
-        # number of polydisperse parameters
-        num_pd = sum(p.length for p in self.kernel_parameters if p.polydisperse)
-        # 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.
-        self.max_pd = min(num_pd, MAX_PD)
-
-        self.npars = sum(p.length for p in self.kernel_parameters)
-
-        # true if has 2D parameters
-        self.has_2d = any(p.type in ('orientation', 'magnetic')
-                          for p in self.kernel_parameters)
-
-        self.pd_1d = set(p.name for p in self.call_parameters
-                if p.polydisperse and p.type not in ('orientation', 'magnetic'))
-        self.pd_2d = set(p.name for p in self.call_parameters
-                         if p.polydisperse and p.type != 'magnetic')
+        return full_list
 
     def user_parameters(self, pars={}, is2d=True):
+        # type: (Dict[str, float], bool) -> List[str]
         """
         Return the list of parameters for the given data type.
@@ -530,4 +580,5 @@
 
 def isstr(x):
+    # type: (Any) -> bool
     # TODO: 2-3 compatible tests for str, including unicode strings
     return isinstance(x, str)
@@ -557,6 +608,5 @@
     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, 'invariant_info', None)
-    info.demo = getattr(kernel_module, 'demo', None)
+    info.variant_info = getattr(kernel_module, 'variant_info', None)
     info.source = getattr(kernel_module, 'source', [])
     info.tests = getattr(kernel_module, 'tests', [])
@@ -613,31 +663,31 @@
       build complete product and mixture models from just the info.
     """
-    id = None
-    filename = None
-    name = None
-    title = None
-    description = None
-    parameters = None
-    demo = None
-    composition = None
-    docs = None
-    category = None
-    single = None
-    structure_factor = None
-    profile_axes = None
-    variant_info = None
-    demo = None
-    source = None
-    tests = None
-    ER = None
-    VR = None
-    form_volume = None
-    Iq = None
-    Iqxy = None
+    id = None               # type: str
+    filename = None         # type: str
+    name = None             # type: str
+    title = None            # type: str
+    description = None      # type: str
+    parameters = None       # type: ParameterTable
+    demo = None             # type: Dict[str, float]
+    composition = None      # type: Optional[Tuple[str, List[ModelInfo]]]
+    docs = None             # type: str
+    category = None         # type: Optional[str]
+    single = None           # type: bool
+    structure_factor = None # type: bool
+    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, ...], float]]
+    VR = None               # type: Optional[Callable[[np.ndarray, ...], float]]
+    form_volume = None      # type: Optional[Callable[[np.ndarray, ...], float]]
+    Iq = None               # type: Optional[Callable[[np.ndarray, ...], np.ndarray]]
+    Iqxy = None             # type: Optional[Callable[[np.ndarray, ...], np.ndarray]]
     profile = None
     sesans = None
-    mono_details = None
+    mono_details = None     # type: CallDetails
 
     def __init__(self):
+        # type: () -> None
         pass
 
