Index: sasmodels/core.py
===================================================================
--- sasmodels/core.py	(revision b0de25217231a59b280fb18c92467f20fb38c947)
+++ sasmodels/core.py	(revision d92182f88244a4ce39a4ef855ac8295e073ff65f)
@@ -64,12 +64,13 @@
         * all: all models
         * py: python models only
-        * c: compiled models only
-        * single: models which support single precision
-        * double: models which require double precision
-        * opencl: controls if OpenCL is supperessed
-        * 1d: models which are 1D only, or 2D using abs(q)
-        * 2d: models which can be 2D
-        * magnetic: models with an sld
-        * nommagnetic: models without an sld
+        * c: c models only
+        * single: c models which support single precision
+        * double: c models which require double precision
+        * opencl: c models which run in opencl
+        * dll: c models which do not run in opencl
+        * 1d: models without orientation
+        * 2d: models with orientation
+        * magnetic: models supporting magnetic sld
+        * nommagnetic: models without magnetic parameter
 
     For multiple conditions, combine with plus.  For example, *c+single+2d*
@@ -95,22 +96,26 @@
     info = load_model_info(name)
     pars = info.parameters.kernel_parameters
-    if kind == "py" and callable(info.Iq):
-        return True
-    elif kind == "c" and not callable(info.Iq):
-        return True
-    elif kind == "double" and not info.single:
-        return True
-    elif kind == "single" and info.single:
-        return True
-    elif kind == "opencl" and info.opencl:
-        return True
-    elif kind == "2d" and any(p.type == 'orientation' for p in pars):
-        return True
-    elif kind == "1d" and all(p.type != 'orientation' for p in pars):
-        return True
-    elif kind == "magnetic" and any(p.type == 'sld' for p in pars):
-        return True
-    elif kind == "nonmagnetic" and any(p.type != 'sld' for p in pars):
-        return True
+    # TODO: may be adding Fq to the list at some point
+    is_pure_py = callable(info.Iq)
+    if kind == "py":
+        return is_pure_py
+    elif kind == "c":
+        return not is_pure_py
+    elif kind == "double":
+        return not info.single and not is_pure_py
+    elif kind == "single":
+        return info.single and not is_pure_py
+    elif kind == "opencl":
+        return info.opencl
+    elif kind == "dll":
+        return not info.opencl and not is_pure_py
+    elif kind == "2d":
+        return any(p.type == 'orientation' for p in pars)
+    elif kind == "1d":
+        return all(p.type != 'orientation' for p in pars)
+    elif kind == "magnetic":
+        return any(p.type == 'sld' for p in pars)
+    elif kind == "nonmagnetic":
+        return not any(p.type == 'sld' for p in pars)
     return False
 
@@ -317,14 +322,4 @@
     return numpy_dtype, fast, platform
 
-def list_models_main():
-    # type: () -> None
-    """
-    Run list_models as a main program.  See :func:`list_models` for the
-    kinds of models that can be requested on the command line.
-    """
-    import sys
-    kind = sys.argv[1] if len(sys.argv) > 1 else "all"
-    print("\n".join(list_models(kind)))
-
 def test_composite_order():
     def test_models(fst, snd):
@@ -386,4 +381,20 @@
     assert target == actual, "%s != %s"%(target, actual)
 
+def list_models_main():
+    # type: () -> None
+    """
+    Run list_models as a main program.  See :func:`list_models` for the
+    kinds of models that can be requested on the command line.
+    """
+    import sys
+    kind = sys.argv[1] if len(sys.argv) > 1 else "all"
+    try:
+        models = list_models(kind)
+    except Exception as exc:
+        print(list_models.__doc__)
+        return 1
+
+    print("\n".join(list_models(kind)))
+
 if __name__ == "__main__":
     list_models_main()
Index: sasmodels/model_test.py
===================================================================
--- sasmodels/model_test.py	(revision 00afc158f4641f755fc8518e7528d23f8655d30b)
+++ sasmodels/model_test.py	(revision d92182f88244a4ce39a4ef855ac8295e073ff65f)
@@ -5,7 +5,9 @@
 Usage::
 
-    python -m sasmodels.model_test [opencl|cuda|dll] model1 model2 ...
-
-    if model1 is 'all', then all except the remaining models will be tested
+    python -m sasmodels.model_test [opencl|cuda|dll|all] model1 model2 ...
+
+If model1 is 'all', then all except the remaining models will be tested.
+Subgroups are also possible, such as 'py', 'single' or '1d'.  See
+:func:`core.list_models` for details.
 
 Each model is tested using the default parameters at q=0.1, (qx, qy)=(0.1, 0.1),
@@ -90,8 +92,10 @@
     suite = unittest.TestSuite()
 
-    if models[0] in core.KINDS:
+    try:
+        # See if the first model parses as a model group
+        group = list_models(models[0])
         skip = models[1:]
-        models = list_models(models[0])
-    else:
+        models = group
+    except Exception:
         skip = []
     for model_name in models:
@@ -475,57 +479,4 @@
 
 
-def main():
-    # type: (*str) -> int
-    """
-    Run tests given is models.
-
-    Returns 0 if success or 1 if any tests fail.
-    """
-    try:
-        from xmlrunner import XMLTestRunner as TestRunner
-        test_args = {'output': 'logs'}
-    except ImportError:
-        from unittest import TextTestRunner as TestRunner
-        test_args = {}
-
-    parser = argparse.ArgumentParser(description="Test SasModels Models")
-    parser.add_argument("-v", "--verbose", action="store_const",
-                        default=1, const=2, help="Use verbose output")
-    parser.add_argument("engine", metavar="[engine]",
-                        help="Engines on which to run the test.  "
-                        "Valid values are opencl, dll, and opencl_and_dll. "
-                        "Defaults to opencl_and_dll if no value is given")
-    parser.add_argument("models", nargs="*",
-                        help='The names of the models to be tested.  '
-                        'If the first model is "all", then all except the '
-                        'remaining models will be tested.')
-    args, models = parser.parse_known_args()
-
-    if args.engine == "opencl":
-        if not use_opencl():
-            print("opencl is not available")
-            return 1
-        loaders = ['opencl']
-    elif args.engine == "dll":
-        loaders = ["dll"]
-    elif args.engine == "cuda":
-        if not use_cuda():
-            print("cuda is not available")
-            return 1
-        loaders = ['cuda']
-    else:
-        # Default to running both engines
-        loaders = ['dll']
-        if use_opencl():
-            loaders.append('opencl')
-        if use_cuda():
-            loaders.append('cuda')
-        args.models.insert(0, args.engine)
-
-    runner = TestRunner(verbosity=args.verbose, **test_args)
-    result = runner.run(make_suite(loaders, args.models))
-    return 1 if result.failures or result.errors else 0
-
-
 def model_tests():
     # type: () -> Iterator[Callable[[], None]]
@@ -562,4 +513,59 @@
 
 
+def main():
+    # type: (*str) -> int
+    """
+    Run tests given is models.
+
+    Returns 0 if success or 1 if any tests fail.
+    """
+    try:
+        from xmlrunner import XMLTestRunner as TestRunner
+        test_args = {'output': 'logs'}
+    except ImportError:
+        from unittest import TextTestRunner as TestRunner
+        test_args = {}
+
+    parser = argparse.ArgumentParser(description="Test SasModels Models")
+    parser.add_argument("-v", "--verbose", action="store_const",
+                        default=1, const=2, help="Use verbose output")
+    parser.add_argument("-e", "--engine", default="all",
+                        help="Engines on which to run the test.  "
+                        "Valid values are opencl, cuda, dll, and all. "
+                        "Defaults to all if no value is given")
+    parser.add_argument("models", nargs="*",
+                        help="The names of the models to be tested.  "
+                        "If the first model is 'all', then all but the listed "
+                        "models will be tested.  See core.list_models() for "
+                        "names of other groups, such as 'py' or 'single'.")
+    args, models = parser.parse_known_args()
+
+    if args.engine == "opencl":
+        if not use_opencl():
+            print("opencl is not available")
+            return 1
+        loaders = ['opencl']
+    elif args.engine == "dll":
+        loaders = ["dll"]
+    elif args.engine == "cuda":
+        if not use_cuda():
+            print("cuda is not available")
+            return 1
+        loaders = ['cuda']
+    elif args.engine == "all":
+        loaders = ['dll']
+        if use_opencl():
+            loaders.append('opencl')
+        if use_cuda():
+            loaders.append('cuda')
+    else:
+        print("unknown engine " + args.engine)
+        return 1
+
+    runner = TestRunner(verbosity=args.verbose, **test_args)
+    result = runner.run(make_suite(loaders, args.models))
+    return 1 if result.failures or result.errors else 0
+
+
 if __name__ == "__main__":
     sys.exit(main())
Index: sasmodels/modelinfo.py
===================================================================
--- sasmodels/modelinfo.py	(revision 4f4d3e3574a18563fda8a9f5e84a9106aa7a1f1e)
+++ sasmodels/modelinfo.py	(revision d92182f88244a4ce39a4ef855ac8295e073ff65f)
@@ -985,4 +985,9 @@
     #: C code, either defined as a string, or in the sources.
     shell_volume = None      # type: Union[None, str, Callable[[np.ndarray], float]]
+    #: Computes the effective radius of the shape given the volume parameters.
+    #: Only needed for models defined in python that can be used for
+    #: monodisperse approximation for non-dilute solutions, P@S.  The first
+    #: argument is the integer effective radius mode, with default 0.
+    effective_radius = None  # type: Union[None, Callable[[int, 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
@@ -996,5 +1001,7 @@
     #: 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.
+    #: include the decimal point. See :mod:`generate` for more details. If
+    #: *have_Fq* is True, then Iq should return an interleaved array of 
+    #: $[\sum F(q_1), \sum F^2(q_1), \ldots, \sum F(q_n), \sum F^2(q_n)]$.
     Iq = None               # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
     #: Returns *I(qab, qc, a, b, ...)*.  The interface follows :attr:`Iq`.
