Index: sasmodels/modelinfo.py
===================================================================
--- sasmodels/modelinfo.py	(revision 7ae2b7f85895f1eddd6ef18f37ff501467a34961)
+++ sasmodels/modelinfo.py	(revision 04045f41855bd4054e1eb902a6934d1c403980a2)
@@ -7,4 +7,6 @@
 module into the model info block as seen by the rest of the sasmodels library.
 """
+from __future__ import print_function
+
 from copy import copy
 from os.path import abspath, basename, splitext
@@ -16,5 +18,5 @@
 # Optional typing
 try:
-    from typing import Tuple, List, Union, Dict, Optional, Any, Callable
+    from typing import Tuple, List, Union, Dict, Optional, Any, Callable, Sequence, Set
 except ImportError:
     pass
@@ -22,5 +24,5 @@
     from .details import CallDetails
     Limits = Tuple[float, float]
-    #LimitsOrChoice = Union[Limits, Tuple[str]]
+    LimitsOrChoice = Union[Limits, Tuple[Sequence[str]]]
     ParameterDef = Tuple[str, str, float, Limits, str, str]
     ParameterSetUser = Dict[str, Union[float, List[float]]]
@@ -68,6 +70,6 @@
 
 def parse_parameter(name, units='', default=np.NaN,
-                    limits=(-np.inf, np.inf), ptype='', description=''):
-    # type: (str, str, float, Limits, str, str) -> Parameter
+                    user_limits=None, ptype='', description=''):
+    # type: (str, str, float, LimitsOrChoice, str, str) -> Parameter
     """
     Parse an individual parameter from the parameter definition block.
@@ -81,36 +83,44 @@
     if not isstr(units):
         raise ValueError("expected units to be a string for %s"%name)
-    # if limits is a list of strings, then this is a choice list
-    # field, and limits are 1 to length of string list
+
+    # Process limits as [float, float] or [[str, str, ...]]
     choices = []  # type: List[str]
-    if isinstance(limits, list) and all(isstr(k) for k in limits):
-        choices = limits
-        limits = (0., len(choices)-1.)
-
-    # TODO: maybe allow limits of None for (-inf, inf)
-    try:
-        low, high = limits
-        if not isinstance(low, (int, float)):
-            raise TypeError("low is not numeric")
-        if not isinstance(high, (int, float)):
-            raise TypeError("high is not numeric")
-        if low >= high:
-            raise ValueError("require low < high")
-    except Exception:
-        raise ValueError("invalid limits %s for %s"%(limits, name))
-
+    if user_limits is None:
+        limits = (-np.inf, np.inf)
+    elif not isinstance(user_limits, (tuple, list)):
+        raise ValueError("invalid limits for %s"%name)
+    else:
+        # if limits is [[str,...]], then this is a choice list field,
+        # and limits are 1 to length of string list
+        if isinstance(user_limits[0], (tuple, list)):
+            choices = user_limits[0]
+            limits = (0., len(choices)-1.)
+            if not all(isstr(k) for k in choices):
+                raise ValueError("choices must be strings for %s"%name)
+        else:
+            try:
+                low, high = user_limits
+                limits = (float(low), float(high))
+            except Exception:
+                raise ValueError("invalid limits for %s"%name)
+            else:
+                if low >= high:
+                    raise ValueError("require lower limit < upper limit")
+
+    # Process default value as float, making sure it is in range
     if not isinstance(default, (int, float)):
         raise ValueError("expected default %r to be a number for %s"
                          % (default, name))
-    if default < low or default > high:
+    if default < limits[0] or default > limits[1]:
         raise ValueError("default value %r not in range for %s"
                          % (default, name))
 
+    # Check for valid parameter type
     if ptype not in ("volume", "orientation", "sld", "magnetic", ""):
         raise ValueError("unexpected type %r for %s" % (ptype, name))
 
+    # Check for valid parameter description
     if not isstr(description):
         raise ValueError("expected description to be a string")
-
 
     # Parameter id for name[n] does not include [n]
@@ -122,5 +132,4 @@
     else:
         pid, ref = name, None
-
 
     # automatically identify sld types
@@ -474,8 +483,5 @@
                     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
+                p.length = int(high)
 
     def _get_defaults(self):
@@ -544,8 +550,10 @@
         early, and rerender the table when it is changed.
         """
+        # control parameters go first
         control = [p for p in self.kernel_parameters if p.is_control]
 
         # Gather entries such as name[n] into groups of the same n
-        dependent = dict((p.id, []) for p in control)  # type: Dict[str, List[Parameter]]
+        dependent = {} # type: Dict[str, List[Parameter]]
+        dependent.update((p.id, []) for p in control)
         for p in self.kernel_parameters:
             if p.length_control is not None:
@@ -635,5 +643,5 @@
     info.single = getattr(kernel_module, 'single', True)
     info.structure_factor = getattr(kernel_module, 'structure_factor', False)
-    info.profile_axes = getattr(kernel_module, 'profile_axes', ['x','y'])
+    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', [])
@@ -647,4 +655,6 @@
     info.profile = getattr(kernel_module, 'profile', None) # type: ignore
     info.sesans = getattr(kernel_module, 'sesans', None) # type: ignore
+    info.control = getattr(kernel_module, 'control', None)
+    info.hidden = getattr(kernel_module, 'hidden', None) # type: ignore
 
     # Precalculate the monodisperse parameter details
@@ -691,4 +701,7 @@
       *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
@@ -703,4 +716,5 @@
     demo = None             # type: Dict[str, float]
     composition = None      # type: Optional[Tuple[str, List[ModelInfo]]]
+    control = None          # type: str
     docs = None             # type: str
     category = None         # type: Optional[str]
@@ -718,4 +732,5 @@
     profile = None          # type: Optional[Callable[[np.ndarray], None]]
     sesans = None           # type: Optional[Callable[[np.ndarray], np.ndarray]]
+    hidden = None           # type: Optional[Callable[int], Set[str]]
     mono_details = None     # type: CallDetails
 
@@ -724,3 +739,14 @@
         pass
 
-
+    def get_hidden_parameters(self, control):
+        if self.hidden is not None:
+            hidden = self.hidden(control)
+        else:
+            controls = [p for p in self.parameters.kernel_parameters]
+            if len(controls) != 1:
+                raise ValueError("more than one control parameter")
+            hidden = set(p.id+str(k)
+                         for p in self.parameters.kernel_parameters
+                         for k in range(control+1, p.length+1)
+                         if p.length > 1)
+        return hidden
