Index: doc/guide/plugin.rst
===================================================================
--- doc/guide/plugin.rst	(revision 81751c29b389be731a04ab84a1a93dedf718e16a)
+++ doc/guide/plugin.rst	(revision 94bfa4260b733d3fccfaf1e8ee85838ce89b4230)
@@ -272,4 +272,15 @@
 structure factor to account for interactions between particles.  See
 `Form_Factors`_ for more details.
+
+**model_info = ...** lets you define a model directly, for example, by
+loading and modifying existing models.  This is done implicitly by
+:func:`sasmodels.core.load_model_info`, which can create a mixture model
+from a pair of existing models.  For example::
+
+    from sasmodels.core import load_model_info
+    model_info = load_model_info('sphere+cylinder')
+
+See :class:`sasmodels.modelinfo.ModelInfo` for details about the model
+attributes that are defined.
 
 Model Parameters
@@ -894,5 +905,5 @@
              - \frac{\sin(x)}{x}\left(\frac{1}{x} - \frac{3!}{x^3} + \frac{5!}{x^5} - \frac{7!}{x^7}\right)
 
-        For small arguments ,
+        For small arguments,
 
         .. math::
Index: example/multiscatfit.py
===================================================================
--- example/multiscatfit.py	(revision 49d1f8b8b50df60b60d3cc8b9dfe3c9c22f24db1)
+++ example/multiscatfit.py	(revision 2c4a190a64dff2df7c3a9d3ea13d6d566b439ecd)
@@ -15,8 +15,8 @@
 
     # Show the model without fitting
-    PYTHONPATH=..:../explore:../../bumps:../../sasview/src python multiscatfit.py
+    PYTHONPATH=..:../../bumps:../../sasview/src python multiscatfit.py
 
     # Run the fit
-    PYTHONPATH=..:../explore:../../bumps:../../sasview/src ../../bumps/run.py \
+    PYTHONPATH=..:../../bumps:../../sasview/src ../../bumps/run.py \
     multiscatfit.py --store=/tmp/t1
 
@@ -55,4 +55,10 @@
     )
 
+# Tie the model to the data
+M = Experiment(data=data, model=model)
+
+# Stack mulitple scattering on top of the existing resolution function.
+M.resolution = MultipleScattering(resolution=M.resolution, probability=0.)
+
 # SET THE FITTING PARAMETERS
 model.radius_polar.range(15, 3000)
@@ -65,22 +71,9 @@
 model.scale.range(0, 0.1)
 
-# Mulitple scattering probability parameter
-# HACK: the probability is stuffed in as an extra parameter to the experiment.
-probability = Parameter(name="probability", value=0.0)
-probability.range(0.0, 0.9)
+# The multiple scattering probability parameter is in the resolution function
+# instead of the scattering function, so access it through M.resolution
+M.scattering_probability.range(0.0, 0.9)
 
-M = Experiment(data=data, model=model, extra_pars={'probability': probability})
-
-# Stack mulitple scattering on top of the existing resolution function.
-# Because resolution functions in sasview don't have fitting parameters,
-# we instead allow the multiple scattering calculator to take a function
-# instead of a probability.  This function returns the current value of
-# the parameter. ** THIS IS TEMPORARY ** when multiple scattering is
-# properly integrated into sasmodels and sasview, its fittable parameter
-# will be treated like the model parameters.
-M.resolution = MultipleScattering(resolution=M.resolution,
-                                  probability=lambda: probability.value,
-                                  )
-M._kernel_inputs = M.resolution.q_calc
+# Let bumps know that we are fitting this experiment
 problem = FitProblem(M)
 
Index: sasmodels/__init__.py
===================================================================
--- sasmodels/__init__.py	(revision a1ec9081782bea4f4d869eec0518e8eeebd50f18)
+++ sasmodels/__init__.py	(revision 37f38ff799a7c19ab534f7e38596447af9647c6c)
@@ -14,5 +14,5 @@
 defining new models.
 """
-__version__ = "0.98"
+__version__ = "0.99"
 
 def data_files():
Index: sasmodels/bumps_model.py
===================================================================
--- sasmodels/bumps_model.py	(revision 49d1f8b8b50df60b60d3cc8b9dfe3c9c22f24db1)
+++ sasmodels/bumps_model.py	(revision 2c4a190a64dff2df7c3a9d3ea13d6d566b439ecd)
@@ -35,4 +35,5 @@
     # when bumps is not on the path.
     from bumps.names import Parameter # type: ignore
+    from bumps.parameter import Reference # type: ignore
 except ImportError:
     pass
@@ -139,4 +140,16 @@
     def __init__(self, data, model, cutoff=1e-5, name=None, extra_pars=None):
         # type: (Data, Model, float) -> None
+        # Allow resolution function to define fittable parameters.  We do this
+        # by creating reference parameters within the resolution object rather
+        # than modifying the object itself to use bumps parameters.  We need
+        # to reset the parameters each time the object has changed.  These
+        # additional parameters need to be returned from the fitting engine.
+        # To make them available to the user, they are added as top-level
+        # attributes to the experiment object.  The only change to the
+        # resolution function is that it needs an optional 'fittable' attribute
+        # which maps the internal name to the user visible name for the
+        # for the parameter.
+        self._resolution = None
+        self._resolution_pars = {}
         # remember inputs so we can inspect from outside
         self.name = data.filename if name is None else name
@@ -145,4 +158,7 @@
         self._interpret_data(data, model.sasmodel)
         self._cache = {}
+        # CRUFT: no longer need extra parameters
+        # Multiple scattering probability is now retrieved directly from the
+        # multiple scattering resolution function.
         self.extra_pars = extra_pars
 
@@ -162,4 +178,27 @@
         return len(self.Iq)
 
+    @property
+    def resolution(self):
+        return self._resolution
+
+    @resolution.setter
+    def resolution(self, value):
+        self._resolution = value
+
+        # Remove old resolution fitting parameters from experiment
+        for name in self._resolution_pars:
+            delattr(self, name)
+
+        # Create new resolution fitting parameters
+        res_pars = getattr(self._resolution, 'fittable', {})
+        self._resolution_pars = {
+            name: Reference(self._resolution, refname, name=name)
+            for refname, name in res_pars.items()
+        }
+
+        # Add new resolution fitting parameters as experiment attributes
+        for name, ref in self._resolution_pars.items():
+            setattr(self, name, ref)
+
     def parameters(self):
         # type: () -> Dict[str, Parameter]
@@ -168,6 +207,7 @@
         """
         pars = self.model.parameters()
-        if self.extra_pars:
+        if self.extra_pars is not None:
             pars.update(self.extra_pars)
+        pars.update(self._resolution_pars)
         return pars
 
Index: sasmodels/direct_model.py
===================================================================
--- sasmodels/direct_model.py	(revision 5024a563075fb86e48bd868acf2f39ea4af2e90b)
+++ sasmodels/direct_model.py	(revision 2c4a190a64dff2df7c3a9d3ea13d6d566b439ecd)
@@ -224,6 +224,4 @@
             else:
                 Iq, dIq = None, None
-            #self._theory = np.zeros_like(q)
-            q_vectors = [res.q_calc]
         elif self.data_type == 'Iqxy':
             #if not model.info.parameters.has_2d:
@@ -242,6 +240,4 @@
             res = resolution2d.Pinhole2D(data=data, index=index,
                                          nsigma=3.0, accuracy=accuracy)
-            #self._theory = np.zeros_like(self.Iq)
-            q_vectors = res.q_calc
         elif self.data_type == 'Iq':
             index = (data.x >= data.qmin) & (data.x <= data.qmax)
@@ -268,7 +264,4 @@
             else:
                 res = resolution.Perfect1D(data.x[index])
-
-            #self._theory = np.zeros_like(self.Iq)
-            q_vectors = [res.q_calc]
         elif self.data_type == 'Iq-oriented':
             index = (data.x >= data.qmin) & (data.x <= data.qmax)
@@ -286,5 +279,4 @@
                                       qx_width=data.dxw[index],
                                       qy_width=data.dxl[index])
-            q_vectors = res.q_calc
         else:
             raise ValueError("Unknown data type") # never gets here
@@ -292,5 +284,4 @@
         # Remember function inputs so we can delay loading the function and
         # so we can save/restore state
-        self._kernel_inputs = q_vectors
         self._kernel = None
         self.Iq, self.dIq, self.index = Iq, dIq, index
@@ -329,5 +320,11 @@
         # type: (ParameterSet, float) -> np.ndarray
         if self._kernel is None:
-            self._kernel = self._model.make_kernel(self._kernel_inputs)
+            # TODO: change interfaces so that resolution returns kernel inputs
+            # Maybe have resolution always return a tuple, or maybe have
+            # make_kernel accept either an ndarray or a pair of ndarrays.
+            kernel_inputs = self.resolution.q_calc
+            if isinstance(kernel_inputs, np.ndarray):
+                kernel_inputs = (kernel_inputs,)
+            self._kernel = self._model.make_kernel(kernel_inputs)
 
         # Need to pull background out of resolution for multiple scattering
Index: sasmodels/multiscat.py
===================================================================
--- sasmodels/multiscat.py	(revision b3703f51bafe67afb8acefe986ad7466720f755b)
+++ sasmodels/multiscat.py	(revision 2c4a190a64dff2df7c3a9d3ea13d6d566b439ecd)
@@ -342,10 +342,8 @@
 
     *probability* is related to the expected number of scattering
-    events in the sample $\lambda$ as $p = 1 = e^{-\lambda}$.  As a
-    hack to allow probability to be a fitted parameter, the "value"
-    can be a function that takes no parameters and returns the current
-    value of the probability.  *coverage* determines how many scattering
-    steps to consider.  The default is 0.99, which sets $n$ such that
-    $1 \ldots n$ covers 99% of the Poisson probability mass function.
+    events in the sample $\lambda$ as $p = 1 - e^{-\lambda}$.
+    *coverage* determines how many scattering steps to consider.  The
+    default is 0.99, which sets $n$ such that $1 \ldots n$ covers 99%
+    of the Poisson probability mass function.
 
     *is2d* is True then 2D scattering is used, otherwise it accepts
@@ -399,5 +397,5 @@
         self.qmin = qmin
         self.nq = nq
-        self.probability = probability
+        self.probability = 0. if probability is None else probability
         self.coverage = coverage
         self.is2d = is2d
@@ -456,4 +454,9 @@
         self.Iqxy = None # type: np.ndarray
 
+        # Label probability as a fittable parameter, and give its external name
+        # Note that the external name must be a valid python identifier, since
+        # is will be set as an experiment attribute.
+        self.fittable = {'probability': 'scattering_probability'}
+
     def apply(self, theory):
         if self.is2d:
@@ -463,4 +466,5 @@
         Iq_calc = Iq_calc.reshape(self.nq, self.nq)
 
+        # CRUFT: don't need probability as a function anymore
         probability = self.probability() if callable(self.probability) else self.probability
         coverage = self.coverage
Index: sasmodels/sasview_model.py
===================================================================
--- sasmodels/sasview_model.py	(revision 3a1afed813c02d7a1016d5e4fe9bfc3cb41c1ae7)
+++ sasmodels/sasview_model.py	(revision 3a1afed813c02d7a1016d5e4fe9bfc3cb41c1ae7)
@@ -25,4 +25,5 @@
 from . import core
 from . import custom
+from . import kernelcl
 from . import product
 from . import generate
@@ -30,4 +31,5 @@
 from . import modelinfo
 from .details import make_kernel_args, dispersion_mesh
+from .kernelcl import reset_environment
 
 # pylint: disable=unused-import
@@ -68,4 +70,16 @@
 #: has changed since we last reloaded.
 _CACHED_MODULE = {}  # type: Dict[str, "module"]
+
+def reset_environment():
+    # type: () -> None
+    """
+    Clear the compute engine context so that the GUI can change devices.
+
+    This removes all compiled kernels, even those that are active on fit
+    pages, but they will be restored the next time they are needed.
+    """
+    kernelcl.reset_environment()
+    for model in MODELS.values():
+        model._model = None
 
 def find_model(modelname):
@@ -696,5 +710,9 @@
     def _calculate_Iq(self, qx, qy=None):
         if self._model is None:
-            self._model = core.build_model(self._model_info)
+            # Only need one copy of the compiled kernel regardless of how many
+            # times it is used, so store it in the class.  Also, to reset the
+            # compute engine, need to clear out all existing compiled kernels,
+            # which is much easier to do if we store them in the class.
+            self.__class__._model = core.build_model(self._model_info)
         if qy is not None:
             q_vectors = [np.asarray(qx), np.asarray(qy)]
