Index: sasmodels/bumps_model.py
===================================================================
--- sasmodels/bumps_model.py	(revision 33d38d57ac52c5b89fc9d1954f5929f5f365d659)
+++ sasmodels/bumps_model.py	(revision 346bc886df88ad4f5c0e3441573497ef436dbf18)
@@ -1,11 +1,44 @@
 """
 Wrap sasmodels for direct use by bumps.
+
+:class:`Model` is a wrapper for the sasmodels kernel which defines a
+bumps *Parameter* box for each kernel parameter.  *Model* accepts keyword
+arguments to set the initial value for each parameter.
+
+:class:`Experiment` combines the *Model* function with a data file loaded by the
+sasview data loader.  *Experiment* takes a *cutoff* parameter controlling
+how far the polydispersity integral extends.
+
+A variety of helper functions are provided:
+
+    :func:`load_data` loads a sasview data file.
+
+    :func:`empty_data1D` creates an empty dataset, which is useful for plotting
+    a theory function before the data is measured.
+
+    :func:`empty_data2D` creates an empty 2D dataset.
+
+    :func:`set_beam_stop` masks the beam stop from the data.
+
+    :func:`set_half` selects the right or left half of the data, which can
+    be useful for shear measurements which have not been properly corrected
+    for path length and reflections.
+
+    :func:`set_top` cuts the top part off the data.
+
+    :func:`plot_data` plots the data file.
+
+    :func:`plot_theory` plots a calculated result from the model.
+
 """
 
 import datetime
+import warnings
 
 import numpy as np
 
 from . import sesans
+from .resolution import Perfect1D, Pinhole1D, Slit1D
+from .resolution2d import Pinhole2D
 
 # CRUFT python 2.6
@@ -20,4 +53,15 @@
 
 
+# CRUFT: old style bumps wrapper which doesn't separate data and model
+def BumpsModel(data, model, cutoff=1e-5, **kw):
+    warnings.warn("Use of BumpsModel is deprecated.  Use bumps_model.Experiment instead.")
+    model = Model(model, **kw)
+    experiment = Experiment(data=data, model=model, cutoff=cutoff)
+    for k in model._parameter_names:
+        setattr(experiment, k, getattr(model, k))
+    return experiment
+
+
+
 def tic():
     """
@@ -42,10 +86,28 @@
     return data
 
-
-def empty_data1D(q):
+def plot_data(data, view='log'):
+    """
+    Plot data loaded by the sasview loader.
+    """
+    if hasattr(data, 'qx_data'):
+        _plot_2d_signal(data, data.data, view=view)
+    else:
+        # Note: kind of weird using the _plot_result1D to plot just the
+        # data, but it handles the masking and graph markup already, so
+        # do not repeat.
+        _plot_result1D(data, None, None, view)
+
+def plot_theory(data, theory, view='log'):
+    if hasattr(data, 'qx_data'):
+        _plot_2d_signal(data, theory, view=view)
+    else:
+        _plot_result1D(data, theory, None, view, include_data=False)
+
+
+def empty_data1D(q, resolution=0.05):
     """
     Create empty 1D data using the given *q* as the x value.
 
-    Resolutions dq/q is 5%.
+    *resolution* dq/q defaults to 5%.
     """
 
@@ -54,11 +116,12 @@
     Iq = 100 * np.ones_like(q)
     dIq = np.sqrt(Iq)
-    data = Data1D(q, Iq, dx=0.05 * q, dy=dIq)
+    data = Data1D(q, Iq, dx=resolution * q, dy=dIq)
     data.filename = "fake data"
     data.qmin, data.qmax = q.min(), q.max()
+    data.mask = np.zeros(len(Iq), dtype='bool')
     return data
 
 
-def empty_data2D(qx, qy=None):
+def empty_data2D(qx, qy=None, resolution=0.05):
     """
     Create empty 2D data using the given mesh.
@@ -66,5 +129,5 @@
     If *qy* is missing, create a square mesh with *qy=qx*.
 
-    Resolution dq/q is 5%.
+    *resolution* dq/q defaults to 5%.
     """
     from sas.dataloader.data_info import Data2D, Detector
@@ -85,8 +148,11 @@
     data.err_data = dIq
     data.mask = mask
+    data.qmin = 1e-16
+    data.qmax = np.inf
 
     # 5% dQ/Q resolution
-    data.dqx_data = 0.05 * Qx
-    data.dqy_data = 0.05 * Qy
+    if resolution != 0:
+        data.dqx_data = resolution * Qx
+        data.dqy_data = resolution * Qy
 
     detector = Detector()
@@ -145,48 +211,5 @@
 
 
-def plot_data(data, Iq, vmin=None, vmax=None, view='log'):
-    """
-    Plot the target value for the data.  This could be the data itself,
-    the theory calculation, or the residuals.
-
-    *scale* can be 'log' for log scale data, or 'linear'.
-    """
-    from numpy.ma import masked_array
-    import matplotlib.pyplot as plt
-    if hasattr(data, 'qx_data'):
-        Iq = Iq + 0
-        valid = np.isfinite(Iq)
-        if view == 'log':
-            valid[valid] = (Iq[valid] > 0)
-            Iq[valid] = np.log10(Iq[valid])
-        elif view == 'q4':
-            Iq[valid] = Iq*(data.qx_data[valid]**2+data.qy_data[valid]**2)**2
-        Iq[~valid | data.mask] = 0
-        #plottable = Iq
-        plottable = masked_array(Iq, ~valid | data.mask)
-        xmin, xmax = min(data.qx_data), max(data.qx_data)
-        ymin, ymax = min(data.qy_data), max(data.qy_data)
-        try:
-            if vmin is None: vmin = Iq[valid & ~data.mask].min()
-            if vmax is None: vmax = Iq[valid & ~data.mask].max()
-        except:
-            vmin, vmax = 0, 1
-        plt.imshow(plottable.reshape(128, 128),
-                   interpolation='nearest', aspect=1, origin='upper',
-                   extent=[xmin, xmax, ymin, ymax], vmin=vmin, vmax=vmax)
-    else: # 1D data
-        if view == 'linear' or view == 'q4':
-            #idx = np.isfinite(Iq)
-            scale = data.x**4 if view == 'q4' else 1.0
-            plt.plot(data.x, scale*Iq) #, '.')
-        else:
-            # Find the values that are finite and positive
-            idx = np.isfinite(Iq)
-            idx[idx] = (Iq[idx] > 0)
-            Iq[~idx] = np.nan
-            plt.loglog(data.x, Iq)
-
-
-def _plot_result1D(data, theory, view):
+def _plot_result1D(data, theory, resid, view, include_data=True):
     """
     Plot the data and residuals for 1D data.
@@ -197,22 +220,32 @@
     #data.y[data.y<0.05] = 0.5
     mdata = masked_array(data.y, data.mask)
-    mdata[np.isnan(mdata)] = masked
+    mdata[~np.isfinite(mdata)] = masked
     if view is 'log':
         mdata[mdata <= 0] = masked
-    mtheory = masked_array(theory, mdata.mask)
-    mresid = masked_array((theory - data.y) / data.dy, mdata.mask)
 
     scale = data.x**4 if view == 'q4' else 1.0
-    plt.subplot(121)
-    plt.errorbar(data.x, scale*mdata, yerr=data.dy)
-    plt.plot(data.x, scale*mtheory, '-', hold=True)
+    if resid is not None:
+        plt.subplot(121)
+
+    if include_data:
+        plt.errorbar(data.x, scale*mdata, yerr=data.dy, fmt='.')
+    if theory is not None:
+        mtheory = masked_array(theory, mdata.mask)
+        plt.plot(data.x, scale*mtheory, '-', hold=True)
+    plt.xscale(view)
     plt.yscale('linear' if view == 'q4' else view)
-    plt.subplot(122)
-    plt.plot(data.x, mresid, 'x')
+    plt.xlabel('Q')
+    plt.ylabel('I(Q)')
+    if resid is not None:
+        mresid = masked_array(resid, mdata.mask)
+        plt.subplot(122)
+        plt.plot(data.x, mresid, 'x')
+        plt.ylabel('residuals')
+        plt.xlabel('Q')
+        plt.xscale(view)
 
 # pylint: disable=unused-argument
-def _plot_sesans(data, theory, view):
+def _plot_sesans(data, theory, resid, view):
     import matplotlib.pyplot as plt
-    resid = (theory - data.y) / data.dy
     plt.subplot(121)
     plt.errorbar(data.x, data.y, yerr=data.dy)
@@ -225,86 +258,75 @@
     plt.ylabel('residuals (P/P0)')
 
-def _plot_result2D(data, theory, view):
+def _plot_result2D(data, theory, resid, view):
     """
     Plot the data and residuals for 2D data.
     """
     import matplotlib.pyplot as plt
-    resid = (theory - data.data) / data.err_data
+    target = data.data[~data.mask]
+    if view == 'log':
+        vmin = min(target[target>0].min(), theory[theory>0].min())
+        vmax = max(target.max(), theory.max())
+    else:
+        vmin = min(target.min(), theory.min())
+        vmax = max(target.max(), theory.max())
+    #print vmin, vmax
     plt.subplot(131)
-    plot_data(data, data.data, view=view)
+    _plot_2d_signal(data, target, view=view, vmin=vmin, vmax=vmax)
+    plt.title('data')
     plt.colorbar()
     plt.subplot(132)
-    plot_data(data, theory, view=view)
+    _plot_2d_signal(data, theory, view=view, vmin=vmin, vmax=vmax)
+    plt.title('theory')
     plt.colorbar()
     plt.subplot(133)
-    plot_data(data, resid, view='linear')
+    _plot_2d_signal(data, resid, view='linear')
+    plt.title('residuals')
     plt.colorbar()
 
-class BumpsModel(object):
-    """
-    Return a bumps wrapper for a SAS model.
-
-    *data* is the data to be fitted.
-
-    *model* is the SAS model from :func:`core.load_model`.
-
-    *cutoff* is the integration cutoff, which avoids computing the
-    the SAS model where the polydispersity weight is low.
-
-    Model parameters can be initialized with additional keyword
-    arguments, or by assigning to model.parameter_name.value.
-
-    The resulting bumps model can be used directly in a FitProblem call.
-    """
-    def __init__(self, data, model, cutoff=1e-5, **kw):
+def _plot_2d_signal(data, signal, vmin=None, vmax=None, view='log'):
+    """
+    Plot the target value for the data.  This could be the data itself,
+    the theory calculation, or the residuals.
+
+    *scale* can be 'log' for log scale data, or 'linear'.
+    """
+    import matplotlib.pyplot as plt
+    from numpy.ma import masked_array
+
+    image = np.zeros_like(data.qx_data)
+    image[~data.mask] = signal
+    valid = np.isfinite(image)
+    if view == 'log':
+        valid[valid] = (image[valid] > 0)
+        image[valid] = np.log10(image[valid])
+    elif view == 'q4':
+        image[valid] *= (data.qx_data[valid]**2+data.qy_data[valid]**2)**2
+    image[~valid | data.mask] = 0
+    #plottable = Iq
+    plottable = masked_array(image, ~valid | data.mask)
+    xmin, xmax = min(data.qx_data), max(data.qx_data)
+    ymin, ymax = min(data.qy_data), max(data.qy_data)
+    # TODO: fix vmin, vmax so it is shared for theory/resid
+    vmin = vmax = None
+    try:
+        if vmin is None: vmin = image[valid & ~data.mask].min()
+        if vmax is None: vmax = image[valid & ~data.mask].max()
+    except:
+        vmin, vmax = 0, 1
+    #print vmin,vmax
+    plt.imshow(plottable.reshape(128, 128),
+               interpolation='nearest', aspect=1, origin='upper',
+               extent=[xmin, xmax, ymin, ymax], vmin=vmin, vmax=vmax)
+
+
+class Model(object):
+    def __init__(self, kernel, **kw):
         from bumps.names import Parameter
 
-        # remember inputs so we can inspect from outside
-        self.data = data
-        self.model = model
-        self.cutoff = cutoff
-        if hasattr(data, 'lam'):
-            self.data_type = 'sesans'
-        elif hasattr(data, 'qx_data'):
-            self.data_type = 'Iqxy'
-        else:
-            self.data_type = 'Iq'
-
-        partype = model.info['partype']
-
-        # interpret data
-        if self.data_type == 'sesans':
-            q = sesans.make_q(data.sample.zacceptance, data.Rmax)
-            self.index = slice(None, None)
-            self.Iq = data.y
-            self.dIq = data.dy
-            self._theory = np.zeros_like(q)
-            q_vectors = [q]
-        elif self.data_type == 'Iqxy':
-            self.index = (data.mask == 0) & (~np.isnan(data.data))
-            self.Iq = data.data[self.index]
-            self.dIq = data.err_data[self.index]
-            self._theory = np.zeros_like(data.data)
-            if not partype['orientation'] and not partype['magnetic']:
-                q_vectors = [np.sqrt(data.qx_data ** 2 + data.qy_data ** 2)]
-            else:
-                q_vectors = [data.qx_data, data.qy_data]
-        elif self.data_type == 'Iq':
-            self.index = (data.x >= data.qmin) & (data.x <= data.qmax) & ~np.isnan(data.y)
-            self.Iq = data.y[self.index]
-            self.dIq = data.dy[self.index]
-            self._theory = np.zeros_like(data.y)
-            q_vectors = [data.x]
-        else:
-            raise ValueError("Unknown data type") # never gets here
-
-        # Remember function inputs so we can delay loading the function and
-        # so we can save/restore state
-        self._fn_inputs = [v[self.index] for v in q_vectors]
-        self._fn = None
-
-        # define bumps parameters
+        self.kernel = kernel
+        partype = kernel.info['partype']
+
         pars = []
-        for p in model.info['parameters']:
+        for p in kernel.info['parameters']:
             name, default, limits = p[0], p[2], p[3]
             value = kw.pop(name, default)
@@ -313,8 +335,8 @@
         for name in partype['pd-2d']:
             for xpart, xdefault, xlimits in [
-                    ('_pd', 0, limits),
-                    ('_pd_n', 35, (0, 1000)),
-                    ('_pd_nsigma', 3, (0, 10)),
-                    ('_pd_type', 'gaussian', None),
+                ('_pd', 0, limits),
+                ('_pd_n', 35, (0, 1000)),
+                ('_pd_nsigma', 3, (0, 10)),
+                ('_pd_type', 'gaussian', None),
                 ]:
                 xname = name + xpart
@@ -328,4 +350,96 @@
             raise TypeError("unexpected parameters: %s"
                             % (", ".join(sorted(kw.keys()))))
+
+    def parameters(self):
+        """
+        Return a dictionary of parameters
+        """
+        return dict((k, getattr(self, k)) for k in self._parameter_names)
+
+class Experiment(object):
+    """
+    Return a bumps wrapper for a SAS model.
+
+    *data* is the data to be fitted.
+
+    *model* is the SAS model from :func:`core.load_model`.
+
+    *cutoff* is the integration cutoff, which avoids computing the
+    the SAS model where the polydispersity weight is low.
+
+    Model parameters can be initialized with additional keyword
+    arguments, or by assigning to model.parameter_name.value.
+
+    The resulting bumps model can be used directly in a FitProblem call.
+    """
+    def __init__(self, data, model, cutoff=1e-5):
+
+        # remember inputs so we can inspect from outside
+        self.data = data
+        self.model = model
+        self.cutoff = cutoff
+        if hasattr(data, 'lam'):
+            self.data_type = 'sesans'
+        elif hasattr(data, 'qx_data'):
+            self.data_type = 'Iqxy'
+        else:
+            self.data_type = 'Iq'
+
+        # interpret data
+        partype = model.kernel.info['partype']
+        if self.data_type == 'sesans':
+            q = sesans.make_q(data.sample.zacceptance, data.Rmax)
+            self.index = slice(None, None)
+            self.Iq = data.y
+            self.dIq = data.dy
+            #self._theory = np.zeros_like(q)
+            q_vectors = [q]
+        elif self.data_type == 'Iqxy':
+            q = np.sqrt(data.qx_data**2 + data.qy_data**2)
+            qmin = getattr(data, 'qmin', 1e-16)
+            qmax = getattr(data, 'qmax', np.inf)
+            self.index = (~data.mask) & (~np.isnan(data.data)) \
+                         & (q >= qmin) & (q <= qmax)
+            self.Iq = data.data[self.index]
+            self.dIq = data.err_data[self.index]
+            self.resolution = Pinhole2D(data=data, index=self.index,
+                                        nsigma=3.0, accuracy='Low')
+            #self._theory = np.zeros_like(self.Iq)
+            if not partype['orientation'] and not partype['magnetic']:
+                raise ValueError("not 2D without orientation or magnetic parameters")
+                #qx,qy = self.resolution.q_calc
+                #q_vectors = [np.sqrt(qx**2 + qy**2)]
+            else:
+                q_vectors = self.resolution.q_calc
+        elif self.data_type == 'Iq':
+            self.index = (data.x >= data.qmin) & (data.x <= data.qmax) & ~np.isnan(data.y)
+            self.Iq = data.y[self.index]
+            self.dIq = data.dy[self.index]
+            if getattr(data, 'dx', None) is not None:
+                q, dq = data.x[self.index], data.dx[self.index]
+                if (dq>0).any():
+                    self.resolution = Pinhole1D(q, dq)
+                else:
+                    self.resolution = Perfect1D(q)
+            elif (getattr(data, 'dxl', None) is not None and
+                  getattr(data, 'dxw', None) is not None):
+                q = data.x[self.index]
+                width = data.dxh[self.index]  # Note: dx
+                self.resolution = Slit1D(data.x[self.index],
+                                         width=data.dxh[self.index],
+                                         height=data.dxw[self.index])
+            else:
+                self.resolution = Perfect1D(data.x[self.index])
+
+            #self._theory = np.zeros_like(self.Iq)
+            q_vectors = [self.resolution.q_calc]
+        else:
+            raise ValueError("Unknown data type") # never gets here
+
+        # Remember function inputs so we can delay loading the function and
+        # so we can save/restore state
+        self._fn_inputs = [v for v in q_vectors]
+        self._fn = None
+
         self.update()
 
@@ -341,31 +455,32 @@
     def parameters(self):
         """
-            Return a dictionary of parameters
-        """
-        return dict((k, getattr(self, k)) for k in self._parameter_names)
+        Return a dictionary of parameters
+        """
+        return self.model.parameters()
 
     def theory(self):
         if 'theory' not in self._cache:
             if self._fn is None:
-                q_input = self.model.make_input(self._fn_inputs)
-                self._fn = self.model(q_input)
-
-            fixed_pars = [getattr(self, p).value for p in self._fn.fixed_pars]
+                q_input = self.model.kernel.make_input(self._fn_inputs)
+                self._fn = self.model.kernel(q_input)
+
+            fixed_pars = [getattr(self.model, p).value for p in self._fn.fixed_pars]
             pd_pars = [self._get_weights(p) for p in self._fn.pd_pars]
             #print fixed_pars,pd_pars
-            self._theory[self.index] = self._fn(fixed_pars, pd_pars, self.cutoff)
+            Iq_calc = self._fn(fixed_pars, pd_pars, self.cutoff)
             #self._theory[:] = self._fn.eval(pars, pd_pars)
             if self.data_type == 'sesans':
                 result = sesans.hankel(self.data.x, self.data.lam * 1e-9,
                                        self.data.sample.thickness / 10,
-                                       self._fn_inputs[0], self._theory)
+                                       self._fn_inputs[0], Iq_calc)
                 self._cache['theory'] = result
             else:
-                self._cache['theory'] = self._theory
+                Iq = self.resolution.apply(Iq_calc)
+                self._cache['theory'] = Iq
         return self._cache['theory']
 
     def residuals(self):
         #if np.any(self.err ==0): print "zeros in err"
-        return (self.theory()[self.index] - self.Iq) / self.dIq
+        return (self.theory() - self.Iq) / self.dIq
 
     def nllf(self):
@@ -381,24 +496,23 @@
         Plot the data and residuals.
         """
-        data, theory = self.data, self.theory()
+        data, theory, resid = self.data, self.theory(), self.residuals()
         if self.data_type == 'Iq':
-            _plot_result1D(data, theory, view)
+            _plot_result1D(data, theory, resid, view)
         elif self.data_type == 'Iqxy':
-            _plot_result2D(data, theory, view)
+            _plot_result2D(data, theory, resid, view)
         elif self.data_type == 'sesans':
-            _plot_sesans(data, theory, view)
+            _plot_sesans(data, theory, resid, view)
         else:
             raise ValueError("Unknown data type")
 
     def simulate_data(self, noise=None):
-        print "noise", noise
-        if noise is None:
-            noise = self.dIq[self.index]
-        else:
-            noise = 0.01 * noise
-            self.dIq[self.index] = noise
-        y = self.theory()
-        y += y * np.random.randn(*y.shape) * noise
+        theory = self.theory()
+        if noise is not None:
+            self.dIq = theory*noise*0.01
+        dy = self.dIq
+        y = theory + np.random.randn(*dy.shape) * dy
+        self.Iq = y
         if self.data_type == 'Iq':
+            self.data.dy[self.index] = dy
             self.data.y[self.index] = y
         elif self.data_type == 'Iqxy':
@@ -418,8 +532,8 @@
         from . import weights
 
-        relative = self.model.info['partype']['pd-rel']
-        limits = self.model.info['limits']
+        relative = self.model.kernel.info['partype']['pd-rel']
+        limits = self.model.kernel.info['limits']
         disperser, value, npts, width, nsigma = [
-            getattr(self, par + ext)
+            getattr(self.model, par + ext)
             for ext in ('_pd_type', '', '_pd_n', '_pd', '_pd_nsigma')]
         value, weight = weights.get_weights(
@@ -442,5 +556,5 @@
     data = load_data('DEC07086.DAT')
     set_beam_stop(data, 0.004)
-    plot_data(data, data.data)
+    plot_data(data)
     import matplotlib.pyplot as plt; plt.show()
 
