Index: sascomp
===================================================================
--- sascomp	(revision 6708a6a1d1edd3ff91e14ee2497f44886a785de4)
+++ sascomp	(revision 424fe00c428dbe537e7616afc49f2450cc8bbc69)
@@ -19,5 +19,5 @@
 
     import sasmodels.compare
-    sasmodels.compare.main()
+    sasmodels.compare.main(*sys.argv[1:])
 
 if __name__ == "__main__":
Index: sasmodels/__init__.py
===================================================================
--- sasmodels/__init__.py	(revision b2377b0e72c7d3e6a6a59e65a0a5d36e50e8bdfe)
+++ sasmodels/__init__.py	(revision 79906d176a181062796b0f1d86b3a542ee0966d9)
@@ -14,5 +14,5 @@
 defining new models.
 """
-__version__ = "0.93"
+__version__ = "0.94"
 
 def data_files():
Index: sasmodels/compare.py
===================================================================
--- sasmodels/compare.py	(revision 050c2c8abb4eeedec15c3dc03d57bcbc440e5adc)
+++ sasmodels/compare.py	(revision 050c2c8abb4eeedec15c3dc03d57bcbc440e5adc)
@@ -789,26 +789,26 @@
 
 
-def parse_opts():
-    # type: () -> Dict[str, Any]
+def parse_opts(argv):
+    # type: (List[str]) -> Dict[str, Any]
     """
     Parse command line options.
     """
     MODELS = core.list_models()
-    flags = [arg for arg in sys.argv[1:]
+    flags = [arg for arg in argv
              if arg.startswith('-')]
-    values = [arg for arg in sys.argv[1:]
+    values = [arg for arg in argv
               if not arg.startswith('-') and '=' in arg]
-    args = [arg for arg in sys.argv[1:]
+    positional_args = [arg for arg in argv
             if not arg.startswith('-') and '=' not in arg]
     models = "\n    ".join("%-15s"%v for v in MODELS)
-    if len(args) == 0:
+    if len(positional_args) == 0:
         print(USAGE)
         print("\nAvailable models:")
         print(columnize(MODELS, indent="  "))
-        sys.exit(1)
-    if len(args) > 3:
+        return None
+    if len(positional_args) > 3:
         print("expected parameters: model N1 N2")
 
-    name = args[0]
+    name = positional_args[0]
     try:
         model_info = core.load_model_info(name)
@@ -816,5 +816,5 @@
         print(str(exc))
         print("Could not find model; use one of:\n    " + models)
-        sys.exit(1)
+        return None
 
     invalid = [o[1:] for o in flags
@@ -823,5 +823,5 @@
     if invalid:
         print("Invalid options: %s"%(", ".join(invalid)))
-        sys.exit(1)
+        return None
 
 
@@ -898,6 +898,6 @@
         del engines[2:]
 
-    n1 = int(args[1]) if len(args) > 1 else 1
-    n2 = int(args[2]) if len(args) > 2 else 1
+    n1 = int(positional_args[1]) if len(positional_args) > 1 else 1
+    n2 = int(positional_args[2]) if len(positional_args) > 2 else 1
     use_sasview = any(engine == 'sasview' and count > 0
                       for engine, count in zip(engines, [n1, n2]))
@@ -916,5 +916,5 @@
             s = set(p.split('_pd')[0] for p in pars)
             print("%r invalid; parameters are: %s"%(k, ", ".join(sorted(s))))
-            sys.exit(1)
+            return None
         presets[k] = float(v) if not k.endswith('type') else v
 
@@ -970,8 +970,9 @@
     from bumps.gui.app_frame import AppFrame  # type: ignore
 
+    is_mac = "cocoa" in wx.version()
+    # Create an app if not running embedded
+    app = wx.App() if wx.GetApp() is None else None
     problem = FitProblem(Explore(opts))
-    is_mac = "cocoa" in wx.version()
-    app = wx.App()
-    frame = AppFrame(parent=None, title="explore")
+    frame = AppFrame(parent=None, title="explore", size=(1000,700))
     if not is_mac: frame.Show()
     frame.panel.set_model(model=problem)
@@ -979,5 +980,6 @@
     frame.panel.aui.Split(0, wx.TOP)
     if is_mac: frame.Show()
-    app.MainLoop()
+    # If running withing an app, start the main loop
+    if app: app.MainLoop()
 
 class Explore(object):
@@ -1048,15 +1050,16 @@
 
 
-def main():
-    # type: () -> None
+def main(*argv):
+    # type: (*str) -> None
     """
     Main program.
     """
-    opts = parse_opts()
-    if opts['explore']:
-        explore(opts)
-    else:
-        compare(opts)
+    opts = parse_opts(argv)
+    if opts is not None:
+        if opts['explore']:
+            explore(opts)
+        else:
+            compare(opts)
 
 if __name__ == "__main__":
-    main()
+    main(*sys.argv[1:])
Index: sasmodels/compare_many.py
===================================================================
--- sasmodels/compare_many.py	(revision 40a87fa5096610e24c3ead10299f609e99f093f2)
+++ sasmodels/compare_many.py	(revision 424fe00c428dbe537e7616afc49f2450cc8bbc69)
@@ -229,30 +229,30 @@
     print_models()
 
-def main():
+def main(argv):
     """
     Main program.
     """
-    if len(sys.argv) not in (6, 7):
+    if len(argv) not in (5, 6):
         print_help()
-        sys.exit(1)
-
-    model = sys.argv[1]
+        return
+
+    model = argv[0]
     if not (model in MODELS) and (model != "all"):
         print('Bad model %s.  Use "all" or one of:'%model)
         print_models()
-        sys.exit(1)
+        return
     try:
-        count = int(sys.argv[2])
-        is2D = sys.argv[3].startswith('2d')
-        assert sys.argv[3][1] == 'd'
-        Nq = int(sys.argv[3][2:])
-        mono = sys.argv[4] == 'mono'
-        cutoff = float(sys.argv[4]) if not mono else 0
-        base = sys.argv[5]
-        comp = sys.argv[6] if len(sys.argv) > 6 else "sasview"
+        count = int(argv[1])
+        is2D = argv[2].startswith('2d')
+        assert argv[2][1] == 'd'
+        Nq = int(argv[2][2:])
+        mono = argv[3] == 'mono'
+        cutoff = float(argv[3]) if not mono else 0
+        base = argv[4]
+        comp = argv[5] if len(argv) > 5 else "sasview"
     except Exception:
         traceback.print_exc()
         print_usage()
-        sys.exit(1)
+        return
 
     data, index = make_data({'qmax':1.0, 'is2d':is2D, 'nq':Nq, 'res':0.,
@@ -265,4 +265,4 @@
 if __name__ == "__main__":
     #from .compare import push_seed
-    #with push_seed(1): main()
-    main()
+    #with push_seed(1): main(sys.argv[1:])
+    main(sys.argv[1:])
Index: sasmodels/custom/__init__.py
===================================================================
--- sasmodels/custom/__init__.py	(revision 40a87fa5096610e24c3ead10299f609e99f093f2)
+++ sasmodels/custom/__init__.py	(revision 2a0c7a685e857b3f7d3721e9eaf84f3854af4a75)
@@ -17,5 +17,5 @@
     def load_module_from_path(fullname, path):
         """load module from *path* as *fullname*"""
-        spec = spec_from_file_location(fullname, path)
+        spec = spec_from_file_location(fullname, os.path.expanduser(path))
         module = module_from_spec(spec)
         spec.loader.exec_module(module)
@@ -26,5 +26,5 @@
     def load_module_from_path(fullname, path):
         """load module from *path* as *fullname*"""
-        module = imp.load_source(fullname, path)
+        module = imp.load_source(fullname, os.path.expanduser(path))
         #os.unlink(path+"c")  # remove the automatic pyc file
         return module
@@ -35,4 +35,5 @@
     name = basename(splitext(path)[0])
     # Placing the model in the 'sasmodels.custom' name space.
-    kernel_module = load_module_from_path('sasmodels.custom.'+name, path)
+    kernel_module = load_module_from_path('sasmodels.custom.'+name,
+                                          os.path.expanduser(path))
     return kernel_module
Index: sasmodels/direct_model.py
===================================================================
--- sasmodels/direct_model.py	(revision bde38b559fcdc618ebafdbb69cf7af6120ea8466)
+++ sasmodels/direct_model.py	(revision 4cc161e1defe1f4a48c06bc054ee261f1857ef79)
@@ -85,4 +85,7 @@
     if model_info.ER is None:
         return 1.0
+    elif not model_info.parameters.form_volume_parameters:
+        # handle the case where ER is provided but model is not polydisperse
+        return model_info.ER()
     else:
         value, weight = _vol_pars(model_info, pars)
@@ -101,4 +104,7 @@
     if model_info.VR is None:
         return 1.0
+    elif not model_info.parameters.form_volume_parameters:
+        # handle the case where ER is provided but model is not polydisperse
+        return model_info.VR()
     else:
         value, weight = _vol_pars(model_info, pars)
@@ -152,4 +158,5 @@
                 for p in model_info.parameters.call_parameters
                 if p.type == 'volume']
+    #import pylab; pylab.plot(vol_pars[0][0],vol_pars[0][1]); pylab.show()
     value, weight = dispersion_mesh(model_info, vol_pars)
     return value, weight
@@ -395,5 +402,5 @@
     model = build_model(model_info)
     calculator = DirectModel(data, model)
-    pars = dict((k, float(v))
+    pars = dict((k, (float(v) if not k.endswith("_pd_type") else v))
                 for pair in sys.argv[3:]
                 for k, v in [pair.split('=')])
Index: sasmodels/generate.py
===================================================================
--- sasmodels/generate.py	(revision 52ec91efe1982286cb988963ccb41671833fc098)
+++ sasmodels/generate.py	(revision 2e49f9e859e8f4ca13fde7469af66ddd45ae0d5f)
@@ -362,4 +362,5 @@
     for long double precision.
     """
+    source = _fix_tgmath_int(source)
     if dtype == F16:
         fbytes = 2
@@ -391,9 +392,149 @@
     source = re.sub(r'(^|[^a-zA-Z0-9_]c?)double(([248]|16)?($|[^a-zA-Z0-9_]))',
                     r'\1%s\2'%type_name, source)
+    source = _tag_float(source, constant_flag)
+    return source
+
+TGMATH_INT_RE = re.compile(r"""
+(?: # Non-capturing match; not lookbehind since pattern length is variable
+  \b              # word boundary
+   # various math functions
+  (a?(sin|cos|tan)h? | atan2
+   | erfc? | tgamma
+   | exp(2|10|m1)? | log(2|10|1p)? | pow[nr]? | sqrt | rsqrt | rootn
+   | fabs | fmax | fmin
+   )
+  \s*[(]\s*       # open parenthesis
+)
+[+-]?(0|[1-9]\d*) # integer
+(?=               # lookahead match: don't want to move from end of int
+  \s*[,)]         # comma or close parenthesis for end of argument
+)                 # end lookahead
+""", re.VERBOSE)
+def _fix_tgmath_int(source):
+    # type: (str) -> str
+    """
+    Replace f(integer) with f(integer.) for sin, cos, pow, etc.
+
+    OS X OpenCL complains that it can't resolve the type generic calls to
+    the standard math functions when they are called with integer constants,
+    but this does not happen with the Windows Intel driver for example.
+    To avoid confusion on the matrix marketplace, automatically promote
+    integers to floats if we recognize them in the source.
+
+    The specific functions we look for are:
+
+        trigonometric: sin, asin, sinh, asinh, etc., and atan2
+        exponential:   exp, exp2, exp10, expm1, log, log2, log10, logp1
+        power:         pow, pown, powr, sqrt, rsqrt, rootn
+        special:       erf, erfc, tgamma
+        float:         fabs, fmin, fmax
+
+    Note that we don't convert the second argument of dual argument
+    functions: atan2, fmax, fmin, pow, powr.  This could potentially
+    be a problem for pow(x, 2), but that case seems to work without change.
+    """
+    out = TGMATH_INT_RE.sub(r'\g<0>.', source)
+    return out
+
+
+# Floating point regular expression
+#
+# Define parts:
+#
+#    E = [eE][+-]?\d+    : Exponent
+#    P = [.]             : Decimal separator
+#    N = [1-9]\d*        : Natural number, no leading zeros
+#    Z = 0               : Zero
+#    F = \d+             : Fractional number, maybe leading zeros
+#    F? = \d*            : Optional fractional number
+#
+# We want to reject bare natural numbers and bare decimal points, so we
+# need to tediously outline the cases where we have either a fraction or
+# an exponent:
+#
+#   ( ZP | ZPF | ZE | ZPE | ZPFE | NP | NPF | NE | NPE | NPFE | PF | PFE )
+#
+#
+# We can then join cases by making parts optional.  The following are
+# some ways to do this:
+#
+#   ( (Z|N)(P|PF|E|PE|PFE) | PFE? )                   # Split on lead
+#     => ( (Z|N)(PF?|(PF?)?E) | PFE? )
+#   ( ((Z|N)PF?|PF)E? | (Z|N)E)                       # Split on point
+#   ( (ZP|ZPF|NP|NPF|PF) | (Z|ZP|ZPF|N|NP|NPF|PF)E )  # Split on E
+#     => ( ((Z|N)PF?|PF) | ((Z|N)(PF?)? | PF) E )
+FLOAT_RE = re.compile(r"""
+    (?<!\w)  # use negative lookbehind since '.' confuses \b test
+    # use split on lead to match float ( (Z|N)(PF?|(PF?)?E) | PFE? )
+    ( ( 0 | [1-9]\d* )                     # ( ( Z | N )
+      ([.]\d* | ([.]\d*)? [eE][+-]?\d+ )   #   (PF? | (PF?)? E )
+    | [.]\d+ ([eE][+-]?\d+)?               # | PF (E)?
+    )                                      # )
+    (?!\w)  # use negative lookahead since '.' confuses \b test
+    """, re.VERBOSE)
+def _tag_float(source, constant_flag):
     # Convert floating point constants to single by adding 'f' to the end,
     # or long double with an 'L' suffix.  OS/X complains if you don't do this.
-    source = re.sub(r'[^a-zA-Z_](\d*[.]\d+|\d+[.]\d*)([eE][+-]?\d+)?',
-                    r'\g<0>%s'%constant_flag, source)
-    return source
+    out = FLOAT_RE.sub(r'\g<0>%s'%constant_flag, source)
+    #print("in",repr(source),"out",repr(out), constant_flag)
+    return out
+
+def test_tag_float():
+
+    cases="""
+ZP  : 0.
+ZPF : 0.0,0.01,0.1
+Z  E: 0e+001
+ZP E: 0.E0
+ZPFE: 0.13e-031
+NP  : 1., 12.
+NPF : 1.0001, 1.1, 1.0
+N  E: 1e0, 37E-080
+NP E: 1.e0, 37.E-080
+NPFE: 845.017e+22
+ PF : .1, .0, .0100
+ PFE: .6e+9, .82E-004
+# isolated cases
+0.
+1e0
+0.13e-013
+# untouched
+struct3.e3, 03.05.67, 37
+# expressions
+3.75+-1.6e-7-27+13.2
+a3.e2 - 0.
+4*atan(1)
+4.*atan(1.)
+"""
+
+    output="""
+ZP  : 0.f
+ZPF : 0.0f,0.01f,0.1f
+Z  E: 0e+001f
+ZP E: 0.E0f
+ZPFE: 0.13e-031f
+NP  : 1.f, 12.f
+NPF : 1.0001f, 1.1f, 1.0f
+N  E: 1e0f, 37E-080f
+NP E: 1.e0f, 37.E-080f
+NPFE: 845.017e+22f
+ PF : .1f, .0f, .0100f
+ PFE: .6e+9f, .82E-004f
+# isolated cases
+0.f
+1e0f
+0.13e-013f
+# untouched
+struct3.e3, 03.05.67, 37
+# expressions
+3.75f+-1.6e-7f-27+13.2f
+a3.e2 - 0.f
+4*atan(1)
+4.f*atan(1.f)
+"""
+
+    for case_in, case_out in zip(cases.split('\n'), output.split('\n')):
+        out = _tag_float(case_in, 'f')
+        assert case_out == out, "%r => %r"%(case_in, out)
 
 
@@ -706,5 +847,6 @@
     Iq_units = "The returned value is scaled to units of |cm^-1| |sr^-1|, absolute scale."
     Sq_units = "The returned value is a dimensionless structure factor, $S(q)$."
-    docs = convert_section_titles_to_boldface(model_info.docs)
+    docs = model_info.docs if model_info.docs is not None else ""
+    docs = convert_section_titles_to_boldface(docs)
     pars = make_partable(model_info.parameters.COMMON
                          + model_info.parameters.kernel_parameters)
@@ -718,4 +860,43 @@
 
 
+# TODO: need a single source for rst_prolog; it is also in doc/rst_prolog
+RST_PROLOG = """\
+.. |Ang| unicode:: U+212B
+.. |Ang^-1| replace:: |Ang|\ :sup:`-1`
+.. |Ang^2| replace:: |Ang|\ :sup:`2`
+.. |Ang^-2| replace:: |Ang|\ :sup:`-2`
+.. |1e-6Ang^-2| replace:: 10\ :sup:`-6`\ |Ang|\ :sup:`-2`
+.. |Ang^3| replace:: |Ang|\ :sup:`3`
+.. |Ang^-3| replace:: |Ang|\ :sup:`-3`
+.. |Ang^-4| replace:: |Ang|\ :sup:`-4`
+.. |cm^-1| replace:: cm\ :sup:`-1`
+.. |cm^2| replace:: cm\ :sup:`2`
+.. |cm^-2| replace:: cm\ :sup:`-2`
+.. |cm^3| replace:: cm\ :sup:`3`
+.. |1e15cm^3| replace:: 10\ :sup:`15`\ cm\ :sup:`3`
+.. |cm^-3| replace:: cm\ :sup:`-3`
+.. |sr^-1| replace:: sr\ :sup:`-1`
+.. |P0| replace:: P\ :sub:`0`\
+
+.. |equiv| unicode:: U+2261
+.. |noteql| unicode:: U+2260
+.. |TM| unicode:: U+2122
+
+.. |cdot| unicode:: U+00B7
+.. |deg| unicode:: U+00B0
+.. |g/cm^3| replace:: g\ |cdot|\ cm\ :sup:`-3`
+.. |mg/m^2| replace:: mg\ |cdot|\ m\ :sup:`-2`
+.. |fm^2| replace:: fm\ :sup:`2`
+.. |Ang*cm^-1| replace:: |Ang|\ |cdot|\ cm\ :sup:`-1`
+"""
+
+# TODO: make a better fake reference role
+RST_ROLES = """\
+.. role:: ref
+
+.. role:: numref
+
+"""
+
 def make_html(model_info):
     """
@@ -723,5 +904,15 @@
     """
     from . import rst2html
-    return rst2html.convert(make_doc(model_info))
+
+    rst = make_doc(model_info)
+    return rst2html.rst2html("".join((RST_ROLES, RST_PROLOG, rst)))
+
+def view_html(model_name):
+    from . import rst2html
+    from . import modelinfo
+    kernel_module = load_kernel_module(model_name)
+    info = modelinfo.make_model_info(kernel_module)
+    url = "file://"+dirname(info.filename)+"/"
+    rst2html.wxview(make_html(info), url=url)
 
 def demo_time():
Index: sasmodels/kerneldll.py
===================================================================
--- sasmodels/kerneldll.py	(revision 14a15a34e512ac50d2cc517fc8d4243ec0bcd9ce)
+++ sasmodels/kerneldll.py	(revision 3764ec19842ceaa33adeddefb56ac0c42a15274e)
@@ -2,28 +2,40 @@
 DLL driver for C kernels
 
-The global attribute *ALLOW_SINGLE_PRECISION_DLLS* should be set to *True* if
-you wish to allow single precision floating point evaluation for the compiled
-models, otherwise it defaults to *False*.
-
-The compiler command line is stored in the attribute *COMPILE*, with string
-substitutions for %(source)s and %(output)s indicating what to compile and
-where to store it.  The actual command is system dependent.
-
-On windows systems, you have a choice of compilers.  *MinGW* is the GNU
-compiler toolchain, available in packages such as anaconda and PythonXY,
-or available stand alone. This toolchain has had difficulties on some
-systems, and may or may not work for you.  In order to build DLLs, *gcc*
-must be on your path.  If the environment variable *SAS_OPENMP* is given
-then -fopenmp is added to the compiler flags.  This requires a version
-of MinGW compiled with OpenMP support.
-
-An alternative toolchain uses the Microsoft Visual C++ compiler, available
-free from microsoft:
+If the environment variable *SAS_OPENMP* is set, then sasmodels
+will attempt to compile with OpenMP flags so that the model can use all
+available kernels.  This may or may not be available on your compiler
+toolchain.  Depending on operating system and environment.
+
+Windows does not have provide a compiler with the operating system.
+Instead, we assume that TinyCC is installed and available.  This can
+be done with a simple pip command if it is not already available::
+
+    pip install tinycc
+
+If Microsoft Visual C++ is available (because VCINSTALLDIR is
+defined in the environment), then that will be used instead.
+Microsoft Visual C++ for Python is available from Microsoft:
 
     `<http://www.microsoft.com/en-us/download/details.aspx?id=44266>`_
 
-Again, this requires that the compiler is available on your path.  This is
-done by running vcvarsall.bat in a windows terminal.  Install locations are
-system dependent, such as:
+If neither compiler is available, sasmodels will check for *MinGW*,
+the GNU compiler toolchain. This available in packages such as Anaconda
+and PythonXY, or available stand alone. This toolchain has had
+difficulties on some systems, and may or may not work for you.
+
+You can control which compiler to use by setting SAS_COMPILER in the
+environment:
+
+  - tinycc (Windows): use the TinyCC compiler shipped with SasView
+  - msvc (Windows): use the Microsoft Visual C++ compiler
+  - mingw (Windows): use the MinGW GNU cc compiler
+  - unix (Linux): use the system cc compiler.
+  - unix (Mac): use the clang compiler. You will need XCode installed, and
+    the XCode command line tools. Mac comes with OpenCL drivers, so generally
+    this will not be needed.
+
+Both *msvc* and *mingw* require that the compiler is available on your path.
+For *msvc*, this can done by running vcvarsall.bat in a windows terminal.
+Install locations are system dependent, such as:
 
     C:\Program Files (x86)\Common Files\Microsoft\Visual C++ for Python\9.0\vcvarsall.bat
@@ -33,14 +45,23 @@
     C:\Users\yourname\AppData\Local\Programs\Common\Microsoft\Visual C++ for Python\9.0\vcvarsall.bat
 
-And again, the environment variable *SAS_OPENMP* controls whether OpenMP is
-used to compile the C code.  This requires the Microsoft vcomp90.dll library,
-which doesn't seem to be included with the compiler, nor does there appear
-to be a public download location.  There may be one on your machine already
-in a location such as:
+OpenMP for *msvc* requires the Microsoft vcomp90.dll library, which doesn't
+seem to be included with the compiler, nor does there appear to be a public
+download location.  There may be one on your machine already in a location
+such as:
 
     C:\Windows\winsxs\x86_microsoft.vc90.openmp*\vcomp90.dll
 
-If you copy this onto your path, such as the python directory or the install
-directory for this application, then OpenMP should be supported.
+If you copy this to somewhere on your path, such as the python directory or
+the install directory for this application, then OpenMP should be supported.
+
+For full control of the compiler, define a function
+*compile_command(source,output)* which takes the name of the source file
+and the name of the output file and returns a compile command that can be
+evaluated in the shell.  For even more control, replace the entire
+*compile(source,output)* function.
+
+The global attribute *ALLOW_SINGLE_PRECISION_DLLS* should be set to *False* if
+you wish to prevent single precision floating point evaluation for the compiled
+models, otherwise set it defaults to *True*.
 """
 from __future__ import print_function
@@ -90,5 +111,5 @@
     compiler = "unix"
 
-ARCH = "" if sys.maxint > 2**32 else "x86"  # maxint=2**31-1 on 32 bit
+ARCH = "" if ct.sizeof(c_void_p) > 4 else "x86"  # 4 byte pointers on x86
 if compiler == "unix":
     # Generic unix compile
Index: sasmodels/model_test.py
===================================================================
--- sasmodels/model_test.py	(revision 40a87fa5096610e24c3ead10299f609e99f093f2)
+++ sasmodels/model_test.py	(revision 897ca7f478f1466ca3d839c8d89b623c240a431a)
@@ -50,5 +50,6 @@
 import numpy as np  # type: ignore
 
-from .core import list_models, load_model_info, build_model, HAVE_OPENCL
+from . import core
+from .core import list_models, load_model_info, build_model
 from .direct_model import call_kernel, call_ER, call_VR
 from .exception import annotate_exception
@@ -98,5 +99,5 @@
         if is_py:  # kernel implemented in python
             test_name = "Model: %s, Kernel: python"%model_name
-            test_method_name = "test_%s_python" % model_name
+            test_method_name = "test_%s_python" % model_info.id
             test = ModelTestCase(test_name, model_info,
                                  test_method_name,
@@ -106,7 +107,7 @@
         else:   # kernel implemented in C
             # test using opencl if desired and available
-            if 'opencl' in loaders and HAVE_OPENCL:
+            if 'opencl' in loaders and core.HAVE_OPENCL:
                 test_name = "Model: %s, Kernel: OpenCL"%model_name
-                test_method_name = "test_%s_opencl" % model_name
+                test_method_name = "test_%s_opencl" % model_info.id
                 # Using dtype=None so that the models that are only
                 # correct for double precision are not tested using
@@ -122,5 +123,5 @@
             if 'dll' in loaders:
                 test_name = "Model: %s, Kernel: dll"%model_name
-                test_method_name = "test_%s_dll" % model_name
+                test_method_name = "test_%s_dll" % model_info.id
                 test = ModelTestCase(test_name, model_info,
                                      test_method_name,
@@ -249,8 +250,53 @@
     return abs(target-actual)/shift < 1.5*10**-digits
 
-def main():
-    # type: () -> int
-    """
-    Run tests given is sys.argv.
+def run_one(model):
+    # type: (str) -> None
+    """
+    Run the tests for a single model, printing the results to stdout.
+
+    *model* can by a python file, which is handy for checking user defined
+    plugin models.
+    """
+    # Note that running main() directly did not work from within the
+    # wxPython pycrust console.  Instead of the results appearing in the
+    # window they were printed to the underlying console.
+    from unittest.runner import TextTestResult, _WritelnDecorator
+
+    # Build a object to capture and print the test results
+    stream = _WritelnDecorator(sys.stdout)  # Add writeln() method to stream
+    verbosity = 2
+    descriptions = True
+    result = TextTestResult(stream, descriptions, verbosity)
+
+    # Build a test suite containing just the model
+    loaders = ['opencl']
+    models = [model]
+    try:
+        suite = make_suite(loaders, models)
+    except Exception:
+        import traceback
+        stream.writeln(traceback.format_exc())
+        return
+
+    # Run the test suite
+    suite.run(result)
+
+    # Print the failures and errors
+    for _, tb in result.errors:
+        stream.writeln(tb)
+    for _, tb in result.failures:
+        stream.writeln(tb)
+
+    # Check if there are user defined tests.
+    # Yes, it is naughty to peek into the structure of the test suite, and
+    # to assume that it contains only one test.
+    if not suite._tests[0].info.tests:
+        stream.writeln("Note: %s has no user defined tests."%model)
+
+
+def main(*models):
+    # type: (*str) -> int
+    """
+    Run tests given is models.
 
     Returns 0 if success or 1 if any tests fail.
@@ -263,5 +309,4 @@
         test_args = {}
 
-    models = sys.argv[1:]
     if models and models[0] == '-v':
         verbosity = 2
@@ -270,5 +315,5 @@
         verbosity = 1
     if models and models[0] == 'opencl':
-        if not HAVE_OPENCL:
+        if not core.HAVE_OPENCL:
             print("opencl is not available")
             return 1
@@ -318,3 +363,3 @@
 
 if __name__ == "__main__":
-    sys.exit(main())
+    sys.exit(main(*sys.argv[1:]))
Index: sasmodels/models/core_multi_shell.py
===================================================================
--- sasmodels/models/core_multi_shell.py	(revision 8c6fbbc5a184ca7680836191ec0feda46203438a)
+++ sasmodels/models/core_multi_shell.py	(revision 7b68dc576b7995dbf041602384c63c354ea857b5)
@@ -132,5 +132,5 @@
 def ER(radius, n, thickness):
     """Effective radius"""
-    n = n[0]  # n cannot be polydisperse
+    n = int(n[0])  # n cannot be polydisperse
     return np.sum(thickness[:n], axis=0) + radius
 
Index: sasmodels/models/onion.py
===================================================================
--- sasmodels/models/onion.py	(revision 3cd100153d0f7b860d95e16d8ab612cc6b70ea97)
+++ sasmodels/models/onion.py	(revision 7b68dc576b7995dbf041602384c63c354ea857b5)
@@ -368,5 +368,5 @@
 def ER(core_radius, n, thickness):
     """Effective radius"""
-    return np.sum(thickness[:n[0]], axis=0) + core_radius
+    return np.sum(thickness[:int(n[0])], axis=0) + core_radius
 
 demo = {
Index: sasmodels/models/sphere.py
===================================================================
--- sasmodels/models/sphere.py	(revision 9a4811ac2bdb93459d640d111cba9f1016660790)
+++ sasmodels/models/sphere.py	(revision 7e6bea817616b3ff05f1c96c2a11a94853dc6276)
@@ -2,5 +2,4 @@
 For information about polarised and magnetic scattering, see
 the :ref:`magnetism` documentation.
-documentation.
 
 Definition
@@ -16,7 +15,7 @@
 
 where *scale* is a volume fraction, $V$ is the volume of the scatterer,
-$r$ is the radius of the sphere, *background* is the background level and
+$r$ is the radius of the sphere and *background* is the background level.
 *sld* and *sld_solvent* are the scattering length densities (SLDs) of the
-scatterer and the solvent respectively.
+scatterer and the solvent respectively, whose difference is $\Delta\rho$.
 
 Note that if your data is in absolute scale, the *scale* should represent
@@ -91,2 +90,13 @@
             radius=120,
             radius_pd=.2, radius_pd_n=45)
+
+tests = [
+    [{}, 0.2, 0.726362],
+    [{"scale": 1., "background": 0., "sld": 6., "sld_solvent": 1.,
+      "radius": 120., "radius_pd": 0.2, "radius_pd_n":45},
+     0.2, 0.228843],
+    [{"radius": 120., "radius_pd": 0.2, "radius_pd_n":45}, "ER", 120.],
+    [{"radius": 120., "radius_pd": 0.2, "radius_pd_n":45}, "VR", 1.],
+]
+
+
Index: sasmodels/rst2html.py
===================================================================
--- sasmodels/rst2html.py	(revision 40a87fa5096610e24c3ead10299f609e99f093f2)
+++ sasmodels/rst2html.py	(revision b217c71eb4946b935994265074fdaf992f34b66f)
@@ -10,10 +10,41 @@
 from contextlib import contextmanager
 
+# CRUFT: locale.getlocale() fails on some versions of OS X
+# See https://bugs.python.org/issue18378
+import locale
+if hasattr(locale, '_parse_localename'):
+    try:
+        locale._parse_localename('UTF-8')
+    except ValueError:
+        _old_parse_localename = locale._parse_localename
+        def _parse_localename(localename):
+            code = locale.normalize(localename)
+            if code == 'UTF-8':
+                return None, code
+            else:
+                return _old_parse_localename(localename)
+        locale._parse_localename = _parse_localename
+
 from docutils.core import publish_parts
 from docutils.writers.html4css1 import HTMLTranslator
 from docutils.nodes import SkipNode
 
+def wxview(html, url="", size=(850, 540)):
+    import wx
+    from wx.html2 import WebView
+    frame = wx.Frame(None, -1, size=size)
+    view = WebView.New(frame)
+    view.SetPage(html, url)
+    frame.Show()
+    return frame
 
-def rst2html(rst, part="whole", math_output="html"):
+def view_rst(filename):
+    from os.path import expanduser
+    with open(expanduser(filename)) as fid:
+        rst = fid.read()
+    html = rst2html(rst)
+    wxview(html)
+
+def rst2html(rst, part="whole", math_output="mathjax"):
     r"""
     Convert restructured text into simple html.
@@ -44,4 +75,10 @@
     else:
         settings = {"math-output": math_output}
+
+    # TODO: support stylesheets
+    #html_root = "/full/path/to/_static/"
+    #sheets = [html_root+s for s in ["basic.css","classic.css"]]
+    #settings["embed_styesheet"] = True
+    #settings["stylesheet_path"] = sheets
 
     # math2html and mathml do not support \frac12
@@ -104,8 +141,8 @@
     assert replace_dollar(u"and $mid$ too") == u"and :math:`mid` too"
     assert replace_dollar(u"$first$, $mid$, $last$") == u":math:`first`, :math:`mid`, :math:`last`"
-    assert replace_dollar(ur"dollar\$ escape") == u"dollar$ escape"
-    assert replace_dollar(ur"dollar \$escape\$ too") == u"dollar $escape$ too"
+    assert replace_dollar(u"dollar\\$ escape") == u"dollar$ escape"
+    assert replace_dollar(u"dollar \\$escape\\$ too") == u"dollar $escape$ too"
     assert replace_dollar(u"spaces $in the$ math") == u"spaces :math:`in the` math"
-    assert replace_dollar(ur"emb\ $ed$\ ed") == ur"emb\ :math:`ed`\ ed"
+    assert replace_dollar(u"emb\\ $ed$\\ ed") == u"emb\\ :math:`ed`\\ ed"
     assert replace_dollar(u"$first$a") == u"$first$a"
     assert replace_dollar(u"a$last$") == u"a$last$"
Index: sasmodels/weights.py
===================================================================
--- sasmodels/weights.py	(revision f1a8811c147abcdc8d3fb84ee7e8a25fa91be625)
+++ sasmodels/weights.py	(revision f1a8811c147abcdc8d3fb84ee7e8a25fa91be625)
@@ -10,4 +10,6 @@
 import numpy as np  # type: ignore
 from scipy.special import gammaln  # type: ignore
+
+# TODO: include dispersion docs with the disperser models
 
 class Dispersion(object):
