Index: src/sas/models/qsmearing.py
===================================================================
--- src/sas/models/qsmearing.py	(revision 9f7fbd95345068210f07aa5bc9994d51a0415eec)
+++ src/sas/models/qsmearing.py	(revision a3f125f0f3c4ff6ed01529da503fbdd5ccb23b14)
@@ -59,4 +59,5 @@
     if _found_resolution == True:
         return QSmearer(data1D, model)
+        #return pinhole_smear(data1D, model)
 
     # Look for slit smearing data
@@ -81,5 +82,6 @@
     # If we found slit smearing data, return a slit smearer
     if _found_slit == True:
-        return PySlitSmearer(data1D, model)
+        #return SlitSmearer(data1D, model)
+        return slit_smear(data1D, model)
     return None
             
@@ -198,5 +200,5 @@
                 iq_in[len(iq_in) - 1] = iq_in_high[0]
             # Append the extrapolated points to the data points
-            if self.nbins_low > 0:                             
+            if self.nbins_low > 0:
                 iq_in_temp = numpy.append(iq_in_low, iq_in)
             if self.nbins_high > 0:
@@ -586,37 +588,64 @@
 
 
-from .resolution import Slit1D
-class PySlitSmearer(object):
-    def __init__(self, data1D, model = None):
+from .resolution import Slit1D, Pinhole1D
+class PySmear(object):
+    """
+    Wrapper for pure python sasmodels resolution functions.
+    """
+    def __init__(self, resolution, model):
         self.model = model
-
-        q = data1D.x
-        width = data1D.dxw if data1D.dxw is not None else 0
-        height = data1D.dxl if data1D.dxl is not None else 0
-        # TODO: width and height seem to be reversed
-        self.resolution = Slit1D(q, height, width)
-
-    def __call__(self, iq_in, first_bin=0, last_bin=None):
-        if last_bin is None or last_bin >= len(iq_in):
-            last_bin = len(iq_in) - 1
+        self.resolution = resolution
+        self.offset = numpy.searchsorted(self.resolution.q_calc, self.resolution.q[0])
+
+    def apply(self, iq_in, first_bin=0, last_bin=None):
+        """
+        Apply the resolution function to the data.
+
+        Note that this is called with iq_in matching data.x, but with
+        iq_in[first_bin:last_bin] set to theory values for these bins,
+        and the remainder left undefined.  The first_bin, last_bin values
+        should be those returned from get_bin_range.
+
+        The returned value is of the same length as iq_in, with the range
+        first_bin:last_bin set to the resolution smeared values.
+        """
+        if last_bin is None: last_bin = len(iq_in)
+        start, end = first_bin + self.offset, last_bin + self.offset
         q_calc = self.resolution.q_calc
         iq_calc = numpy.empty_like(q_calc)
-        iq_calc[:first_bin] = 0
-        iq_calc[first_bin:last_bin+1] = iq_in
-        if last_bin < len(q_calc)-1:
-            iq_calc[last_bin:] = self.model.evalDistribution(q_calc[last_bin:])
-        iq_out = self.resolution.apply(iq_calc)
-        return iq_out[first_bin:last_bin+1]
+        if start > 0:
+            iq_calc[:start] = self.model.evalDistribution(q_calc[:start])
+        if end+1 < len(q_calc):
+            iq_calc[end+1:] = self.model.evalDistribution(q_calc[end+1:])
+        iq_calc[start:end+1] = iq_in[first_bin:last_bin+1]
+        smeared = self.resolution.apply(iq_calc)
+        return smeared
+    __call__ = apply
 
     def get_bin_range(self, q_min=None, q_max=None):
         """
-
-        :param q_min: minimum q-value to smear
-        :param q_max: maximum q-value to smear
-
-        """
-        # assume the data values are sorted
-        first = numpy.searchsorted(self.resolution.q, q_min)
-        # assume that the resolution is much larger than the q range
-        last = len(self.resolution.q)-1
-        return first, last
+        For a given q_min, q_max, find the corresponding indices in the data.
+
+        Returns first, last.
+
+        Note that these are indexes into q from the data, not the q_calc
+        needed by the resolution function.  Note also that these are the
+        indices, not the range limits.  That is, the complete range will be
+        q[first:last+1].
+        """
+        q = self.resolution.q
+        first = numpy.searchsorted(q, q_min)
+        last = numpy.searchsorted(q, q_max)
+        return first, min(last,len(q)-1)
+
+def slit_smear(data, model=None):
+    q = data.x
+    width = data.dxw if data.dxw is not None else 0
+    height = data.dxl if data.dxl is not None else 0
+    # TODO: width and height seem to be reversed
+    return PySmear(Slit1D(q, height, width), model)
+
+def pinhole_smear(data, model=None):
+    q = data.x
+    width = data.dx if data.dx is not None else 0
+    return PySmear(Pinhole1D(q, width), model)
Index: src/sas/models/resolution.py
===================================================================
--- src/sas/models/resolution.py	(revision 9f7fbd95345068210f07aa5bc9994d51a0415eec)
+++ src/sas/models/resolution.py	(revision a3f125f0f3c4ff6ed01529da503fbdd5ccb23b14)
@@ -56,5 +56,5 @@
     be estimated from the *q* and *q_width*.
     """
-    def __init__(self, q, q_width, q_calc=None):
+    def __init__(self, q, q_width, q_calc=None, nsigma=3):
         #*min_step* is the minimum point spacing to use when computing the
         #underlying model.  It should be on the order of
@@ -68,5 +68,5 @@
         # default to Perfect1D if the pinhole geometry is not defined.
         self.q, self.q_width = q, q_width
-        self.q_calc = pinhole_extend_q(q, q_width) \
+        self.q_calc = pinhole_extend_q(q, q_width, nsigma=nsigma) \
             if q_calc is None else np.sort(q_calc)
         self.weight_matrix = pinhole_resolution(self.q_calc,
@@ -203,5 +203,5 @@
     """
     q_min, q_max = np.min(q - nsigma*q_width), np.max(q + nsigma*q_width)
-    return geometric_extrapolation(q, q_min, q_max)
+    return linear_extrapolation(q, q_min, q_max)
 
 
@@ -267,6 +267,6 @@
     """
     Extrapolate *q* out to [*q_min*, *q_max*] using the step size in *q* as
-    a guide.  Extrapolation below uses steps the same size as the first
-    interval.  Extrapolation above uses steps the same size as the final
+    a guide.  Extrapolation below uses about the same size as the first
+    interval.  Extrapolation above uses about the same size as the final
     interval.
 
@@ -276,11 +276,11 @@
     if q_min < q[0]:
         if q_min <= 0: q_min = q[0]/10
-        delta = q[1] - q[0]
-        q_low = np.arange(q_min, q[0], delta)
+        n_low = np.ceil((q[0]-q_min) / (q[1]-q[0])) if q[1]>q[0] else 15
+        q_low = np.linspace(q_min, q[0], n_low+1)[:-1]
     else:
         q_low = []
     if q_max > q[-1]:
-        delta = q[-1] - q[-2]
-        q_high = np.arange(q[-1]+delta, q_max, delta)
+        n_high = np.ceil((q_max-q[-1]) / (q[-1]-q[-2])) if q[-1]>q[-2] else 15
+        q_high = np.linspace(q[-1], q_max, n_high+1)[1:]
     else:
         q_high = []
@@ -288,5 +288,5 @@
 
 
-def geometric_extrapolation(q, q_min, q_max):
+def geometric_extrapolation(q, q_min, q_max, points_per_decade=None):
     r"""
     Extrapolate *q* to [*q_min*, *q_max*] using geometric steps, with the
@@ -295,6 +295,11 @@
     if *q_min* is zero or less then *q[0]/10* is used instead.
 
-    Starting at $q_1$ and stepping geometrically by $\Delta q$
-    to $q_n$ in $n$ points gives a geometric average of:
+    *points_per_decade* sets the ratio between consecutive steps such
+    that there will be $n$ points used for every factor of 10 increase
+    in *q*.
+
+    If *points_per_decade* is not given, it will be estimated as follows.
+    Starting at $q_1$ and stepping geometrically by $\Delta q$ to $q_n$
+    in $n$ points gives a geometric average of:
 
     .. math::
@@ -315,13 +320,16 @@
     """
     q = np.sort(q)
-    delta_q = (len(q)-1)/(log(q[-1]) - log(q[0]))
+    if points_per_decade is None:
+        log_delta_q = (len(q) - 1) / (log(q[-1]) - log(q[0]))
+    else:
+        log_delta_q = log(10.) / points_per_decade
     if q_min < q[0]:
         if q_min < 0: q_min = q[0]/10
-        n_low = delta_q * (log(q[0])-log(q_min))
+        n_low = log_delta_q * (log(q[0])-log(q_min))
         q_low  = np.logspace(log10(q_min), log10(q[0]), np.ceil(n_low)+1)[:-1]
     else:
         q_low = []
     if q_max > q[-1]:
-        n_high = delta_q * (log(q_max)-log(q[-1]))
+        n_high = log_delta_q * (log(q_max)-log(q[-1]))
         q_high = np.logspace(log10(q[-1]), log10(q_max), np.ceil(n_high)+1)[1:]
     else:
@@ -329,2 +337,566 @@
     return np.concatenate([q_low, q, q_high])
 
+
+############################################################################
+# unit tests
+############################################################################
+import unittest
+
+
+def eval_form(q, form, pars):
+    from sasmodels import core
+    kernel = core.make_kernel(form, [q])
+    theory = core.call_kernel(kernel, pars)
+    kernel.release()
+    return theory
+
+
+def gaussian(q, q0, dq):
+    from numpy import exp, pi
+    return exp(-0.5*((q-q0)/dq)**2)/(sqrt(2*pi)*dq)
+
+
+def romberg_slit_1d(q, delta_qv, form, pars):
+    """
+    Romberg integration for slit resolution.
+
+    This is an adaptive integration technique.  It is called with settings
+    that make it slow to evaluate but give it good accuracy.
+    """
+    from scipy.integrate import romberg
+
+    if any(k not in form.info['defaults'] for k in pars.keys()):
+        keys = set(form.info['defaults'].keys())
+        extra = set(pars.keys()) - keys
+        raise ValueError("bad parameters: [%s] not in [%s]"%
+                         (", ".join(sorted(extra)), ", ".join(sorted(keys))))
+
+    _fn = lambda u, q0: eval_form(sqrt(q0**2 + u**2), form, pars)
+    r = [romberg(_fn, 0, delta_qv[0], args=(qi,),
+                 divmax=100, vec_func=True, tol=0, rtol=1e-8)
+         for qi in q]
+    # r should be [float, ...], but it is [array([float]), array([float]),...]
+    return np.asarray(r).flatten()/delta_qv[0]
+
+
+def romberg_pinhole_1d(q, q_width, form, pars, nsigma=5):
+    """
+    Romberg integration for pinhole resolution.
+
+    This is an adaptive integration technique.  It is called with settings
+    that make it slow to evaluate but give it good accuracy.
+    """
+    from scipy.integrate import romberg
+
+    if any(k not in form.info['defaults'] for k in pars.keys()):
+        keys = set(form.info['defaults'].keys())
+        extra = set(pars.keys()) - keys
+        raise ValueError("bad parameters: [%s] not in [%s]"%
+                         (", ".join(sorted(extra)), ", ".join(sorted(keys))))
+
+    _fn = lambda q, q0, dq: eval_form(q, form, pars)*gaussian(q, q0, dq)
+    r = [romberg(_fn, max(qi-nsigma*dqi,1e-10*q[0]), qi+nsigma*dqi, args=(qi, dqi),
+                 divmax=100, vec_func=True, tol=0, rtol=1e-8)
+         for qi,dqi in zip(q,q_width)]
+    return np.asarray(r).flatten()
+
+
+class ResolutionTest(unittest.TestCase):
+
+    def setUp(self):
+        self.x = 0.001*np.arange(1, 11)
+        self.y = self.Iq(self.x)
+
+    def Iq(self, q):
+        "Linear function for resolution unit test"
+        return 12.0 - 1000.0*q
+
+    def test_perfect(self):
+        """
+        Perfect resolution and no smearing.
+        """
+        resolution = Perfect1D(self.x)
+        theory = self.Iq(resolution.q_calc)
+        output = resolution.apply(theory)
+        np.testing.assert_equal(output, self.y)
+
+    def test_slit_zero(self):
+        """
+        Slit smearing with perfect resolution.
+        """
+        resolution = Slit1D(self.x, width=0, height=0, q_calc=self.x)
+        theory = self.Iq(resolution.q_calc)
+        output = resolution.apply(theory)
+        np.testing.assert_equal(output, self.y)
+
+    @unittest.skip("not yet supported")
+    def test_slit_high(self):
+        """
+        Slit smearing with height 0.005
+        """
+        resolution = Slit1D(self.x, width=0, height=0.005, q_calc=self.x)
+        theory = self.Iq(resolution.q_calc)
+        output = resolution.apply(theory)
+        answer = [ 9.0618, 8.6402, 8.1187, 7.1392, 6.1528,
+                   5.5555, 4.5584, 3.5606, 2.5623, 2.0000 ]
+        np.testing.assert_allclose(output, answer, atol=1e-4)
+
+    @unittest.skip("not yet supported")
+    def test_slit_both_high(self):
+        """
+        Slit smearing with width < 100*height.
+        """
+        q = np.logspace(-4, -1, 10)
+        resolution = Slit1D(q, width=0.2, height=np.inf)
+        theory = 1000*self.Iq(resolution.q_calc**4)
+        output = resolution.apply(theory)
+        answer = [ 8.85785, 8.43012, 7.92687, 6.94566, 6.03660,
+                   5.40363, 4.40655, 3.40880, 2.41058, 2.00000 ]
+        np.testing.assert_allclose(output, answer, atol=1e-4)
+
+    @unittest.skip("not yet supported")
+    def test_slit_wide(self):
+        """
+        Slit smearing with width 0.0002
+        """
+        resolution = Slit1D(self.x, width=0.0002, height=0, q_calc=self.x)
+        theory = self.Iq(resolution.q_calc)
+        output = resolution.apply(theory)
+        answer = [ 11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0 ]
+        np.testing.assert_allclose(output, answer, atol=1e-4)
+
+    @unittest.skip("not yet supported")
+    def test_slit_both_wide(self):
+        """
+        Slit smearing with width > 100*height.
+        """
+        resolution = Slit1D(self.x, width=0.0002, height=0.000001,
+                            q_calc=self.x)
+        theory = self.Iq(resolution.q_calc)
+        output = resolution.apply(theory)
+        answer = [ 11.0, 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0 ]
+        np.testing.assert_allclose(output, answer, atol=1e-4)
+
+    def test_pinhole_zero(self):
+        """
+        Pinhole smearing with perfect resolution
+        """
+        resolution = Pinhole1D(self.x, 0.0*self.x)
+        theory = self.Iq(resolution.q_calc)
+        output = resolution.apply(theory)
+        np.testing.assert_equal(output, self.y)
+
+    def test_pinhole(self):
+        """
+        Pinhole smearing with dQ = 0.001 [Note: not dQ/Q = 0.001]
+        """
+        resolution = Pinhole1D(self.x, 0.001*np.ones_like(self.x),
+                               q_calc=self.x)
+        theory = 12.0-1000.0*resolution.q_calc
+        output = resolution.apply(theory)
+        answer = [ 10.44785079, 9.84991299, 8.98101708,
+                  7.99906585, 6.99998311, 6.00001689,
+                  5.00093415, 4.01898292, 3.15008701, 2.55214921]
+        np.testing.assert_allclose(output, answer, atol=1e-8)
+
+
+class IgorComparisonTest(unittest.TestCase):
+
+    def setUp(self):
+        self.pars = TEST_PARS_PINHOLE_SPHERE
+        from sasmodels import core
+        from sasmodels.models import sphere
+        self.model = core.load_model(sphere, dtype='double')
+
+    def Iq_sphere(self, pars, resolution):
+        from sasmodels import core
+        kernel = core.make_kernel(self.model, [resolution.q_calc])
+        theory = core.call_kernel(kernel, pars)
+        result = resolution.apply(theory)
+        kernel.release()
+        return result
+
+    def compare(self, q, output, answer, tolerance):
+        err = (output - answer)/answer
+        idx = abs(err) >= tolerance
+        problem = zip(q[idx], output[idx], answer[idx], err[idx])
+        print "\n".join(str(v) for v in problem)
+        np.testing.assert_allclose(output, answer, rtol=tolerance)
+
+    def test_perfect(self):
+        """
+        Compare sphere model with NIST Igor SANS, no resolution smearing.
+        """
+        pars = TEST_PARS_SLIT_SPHERE
+        data_string = TEST_DATA_SLIT_SPHERE
+
+        data = np.loadtxt(data_string.split('\n')).T
+        q, width, answer, _ = data
+        resolution = Perfect1D(q)
+        output = self.Iq_sphere(pars, resolution)
+        self.compare(q, output, answer, 1e-6)
+
+    def test_pinhole(self):
+        """
+        Compare pinhole resolution smearing with NIST Igor SANS
+        """
+        pars = TEST_PARS_PINHOLE_SPHERE
+        data_string = TEST_DATA_PINHOLE_SPHERE
+
+        data = np.loadtxt(data_string.split('\n')).T
+        q, q_width, answer = data
+        resolution = Pinhole1D(q, q_width)
+        output = self.Iq_sphere(pars, resolution)
+        # TODO: relative error should be lower
+        self.compare(q, output, answer, 3e-4)
+
+    def test_pinhole_romberg(self):
+        """
+        Compare pinhole resolution smearing with romberg integration result.
+        """
+        pars = TEST_PARS_PINHOLE_SPHERE
+        data_string = TEST_DATA_PINHOLE_SPHERE
+        pars['radius'] *= 5
+        radius = pars['radius']
+
+        data = np.loadtxt(data_string.split('\n')).T
+        q, q_width, answer = data
+        answer = romberg_pinhole_1d(q, q_width, self.model, pars)
+        ## Getting 0.1% requires 5 sigma and 200 points per fringe
+        #q_calc = interpolate(pinhole_extend_q(q, q_width, nsigma=5),
+        #                     2*np.pi/radius/200)
+        #tol = 0.001
+        ## The default 3 sigma and no extra points gets 1%
+        q_calc, tol = None, 0.01
+        resolution = Pinhole1D(q, q_width, q_calc=q_calc)
+        output = self.Iq_sphere(pars, resolution)
+        if 0: # debug plot
+            import matplotlib.pyplot as plt
+            resolution = Perfect1D(q)
+            source = self.Iq_sphere(pars, resolution)
+            plt.loglog(q, source, '.')
+            plt.loglog(q, answer, '-', hold=True)
+            plt.loglog(q, output, '-', hold=True)
+            plt.show()
+        self.compare(q, output, answer, tol)
+
+    def test_slit(self):
+        """
+        Compare slit resolution smearing with NIST Igor SANS
+        """
+        pars = TEST_PARS_SLIT_SPHERE
+        data_string = TEST_DATA_SLIT_SPHERE
+
+        data = np.loadtxt(data_string.split('\n')).T
+        q, delta_qv, _, answer = data
+        resolution = Slit1D(q, width=delta_qv, height=0)
+        output = self.Iq_sphere(pars, resolution)
+        # TODO: eliminate Igor test since it is too inaccurate to be useful.
+        # This means we can eliminate the test data as well, and instead
+        # use a generated q vector.
+        self.compare(q, output, answer, 0.5)
+
+    def test_slit_romberg(self):
+        """
+        Compare slit resolution smearing with romberg integration result.
+        """
+        pars = TEST_PARS_SLIT_SPHERE
+        data_string = TEST_DATA_SLIT_SPHERE
+        radius = pars['radius']
+
+        data = np.loadtxt(data_string.split('\n')).T
+        q, delta_qv, _, answer = data
+        answer = romberg_slit_1d(q, delta_qv, self.model, pars)
+        q_calc = slit_extend_q(interpolate(q, 2*np.pi/radius/20),
+                               delta_qv[0], delta_qv[0])
+        resolution = Slit1D(q, width=delta_qv, height=0, q_calc=q_calc)
+        output = self.Iq_sphere(pars, resolution)
+        # TODO: relative error should be lower
+        self.compare(q, output, answer, 0.025)
+
+    def test_ellipsoid(self):
+        """
+        Compare romberg integration for ellipsoid model.
+        """
+        from .core import load_model
+        pars = {
+            'scale':0.05,
+            'rpolar':500, 'requatorial':15000,
+            'sld':6, 'solvent_sld': 1,
+            }
+        form = load_model('ellipsoid', dtype='double')
+        q = np.logspace(log10(4e-5),log10(2.5e-2), 68)
+        delta_qv = [0.117]
+        resolution = Slit1D(q, width=delta_qv, height=0)
+        answer = romberg_slit_1d(q, delta_qv, form, pars)
+        output = resolution.apply(eval_form(resolution.q_calc, form, pars))
+        # TODO: 10% is too much error; use better algorithm
+        self.compare(q, output, answer, 0.1)
+
+    #TODO: can sas q spacing be too sparse for the resolution calculation?
+    @unittest.skip("suppress sparse data test; not supported by current code")
+    def test_pinhole_sparse(self):
+        """
+        Compare pinhole resolution smearing with NIST Igor SANS on sparse data
+        """
+        pars = TEST_PARS_PINHOLE_SPHERE
+        data_string = TEST_DATA_PINHOLE_SPHERE
+
+        data = np.loadtxt(data_string.split('\n')).T
+        q, q_width, answer = data[:, ::20] # Take every nth point
+        resolution = Pinhole1D(q, q_width)
+        output = self.Iq_sphere(pars, resolution)
+        self.compare(q, output, answer, 1e-6)
+
+
+# pinhole sphere parameters
+TEST_PARS_PINHOLE_SPHERE = {
+    'scale': 1.0, 'background': 0.01,
+    'radius': 60.0, 'sld': 1, 'solvent_sld': 6.3,
+    }
+# Q, dQ, I(Q) calculated by NIST Igor SANS package
+TEST_DATA_PINHOLE_SPHERE = """\
+0.001278 0.0002847 2538.41176383
+0.001562 0.0002905 2536.91820405
+0.001846 0.0002956 2535.13182479
+0.002130 0.0003017 2533.06217813
+0.002414 0.0003087 2530.70378586
+0.002698 0.0003165 2528.05024192
+0.002982 0.0003249 2525.10408349
+0.003266 0.0003340 2521.86667499
+0.003550 0.0003437 2518.33907750
+0.003834 0.0003539 2514.52246995
+0.004118 0.0003646 2510.41798319
+0.004402 0.0003757 2506.02690988
+0.004686 0.0003872 2501.35067884
+0.004970 0.0003990 2496.38678318
+0.005253 0.0004112 2491.16237596
+0.005537 0.0004237 2485.63911673
+0.005821 0.0004365 2479.83657083
+0.006105 0.0004495 2473.75676948
+0.006389 0.0004628 2467.40145990
+0.006673 0.0004762 2460.77293372
+0.006957 0.0004899 2453.86724627
+0.007241 0.0005037 2446.69623838
+0.007525 0.0005177 2439.25775219
+0.007809 0.0005318 2431.55421398
+0.008093 0.0005461 2423.58785521
+0.008377 0.0005605 2415.36158137
+0.008661 0.0005750 2406.87009473
+0.008945 0.0005896 2398.12841186
+0.009229 0.0006044 2389.13360806
+0.009513 0.0006192 2379.88958042
+0.009797 0.0006341 2370.39776774
+0.010080 0.0006491 2360.69528793
+0.010360 0.0006641 2350.85169027
+0.010650 0.0006793 2340.42023633
+0.010930 0.0006945 2330.11206013
+0.011220 0.0007097 2319.20109972
+0.011500 0.0007251 2308.43503981
+0.011780 0.0007404 2297.44820179
+0.012070 0.0007558 2285.83853677
+0.012350 0.0007713 2274.41290746
+0.012640 0.0007868 2262.36219581
+0.012920 0.0008024 2250.51169731
+0.013200 0.0008180 2238.45596231
+0.013490 0.0008336 2225.76495666
+0.013770 0.0008493 2213.29618391
+0.014060 0.0008650 2200.19110751
+0.014340 0.0008807 2187.34050325
+0.014620 0.0008965 2174.30529864
+0.014910 0.0009123 2160.61632548
+0.015190 0.0009281 2147.21038112
+0.015470 0.0009440 2133.62023580
+0.015760 0.0009598 2119.37907426
+0.016040 0.0009757 2105.45234903
+0.016330 0.0009916 2090.86319102
+0.016610 0.0010080 2076.60576032
+0.016890 0.0010240 2062.19214565
+0.017180 0.0010390 2047.10550219
+0.017460 0.0010550 2032.38715621
+0.017740 0.0010710 2017.52560123
+0.018030 0.0010880 2001.99124318
+0.018310 0.0011040 1986.84662060
+0.018600 0.0011200 1971.03389745
+0.018880 0.0011360 1955.61395119
+0.019160 0.0011520 1940.08291563
+0.019450 0.0011680 1923.87672225
+0.019730 0.0011840 1908.10656374
+0.020020 0.0012000 1891.66297192
+0.020300 0.0012160 1875.66789021
+0.020580 0.0012320 1859.56357196
+0.020870 0.0012490 1842.79468290
+0.021150 0.0012650 1826.50064489
+0.021430 0.0012810 1810.11533702
+0.021720 0.0012970 1793.06840882
+0.022000 0.0013130 1776.51153580
+0.022280 0.0013290 1759.87201249
+0.022570 0.0013460 1742.57354412
+0.022850 0.0013620 1725.79397319
+0.023140 0.0013780 1708.35831550
+0.023420 0.0013940 1691.45256069
+0.023700 0.0014110 1674.48561783
+0.023990 0.0014270 1656.86525366
+0.024270 0.0014430 1639.79847285
+0.024550 0.0014590 1622.68887088
+0.024840 0.0014760 1604.96421100
+0.025120 0.0014920 1587.85768129
+0.025410 0.0015080 1569.99297335
+0.025690 0.0015240 1552.84580279
+0.025970 0.0015410 1535.54074115
+0.026260 0.0015570 1517.75249337
+0.026540 0.0015730 1500.40115023
+0.026820 0.0015900 1483.03632237
+0.027110 0.0016060 1465.05942429
+0.027390 0.0016220 1447.67682181
+0.027670 0.0016390 1430.46495191
+0.027960 0.0016550 1412.49232282
+0.028240 0.0016710 1395.13182318
+0.028520 0.0016880 1377.93439837
+0.028810 0.0017040 1359.99528971
+0.029090 0.0017200 1342.67274512
+0.029370 0.0017370 1325.55375609
+"""
+
+# Slit sphere parameters
+TEST_PARS_SLIT_SPHERE = {
+    'scale': 0.01, 'background': 0.01,
+    'radius': 60000, 'sld': 1, 'solvent_sld': 4,
+    }
+# Q dQ I(Q) I_smeared(Q)
+TEST_DATA_SLIT_SPHERE = """\
+2.26097e-05 0.117 5.5781372896e+09 1.4626077708e+06
+2.53847e-05 0.117 5.0363141458e+09 1.3117318023e+06
+2.81597e-05 0.117 4.4850108103e+09 1.1594863713e+06
+3.09347e-05 0.117 3.9364658459e+09 1.0094881630e+06
+3.37097e-05 0.117 3.4019975074e+09 8.6518941303e+05
+3.92597e-05 0.117 2.4139519814e+09 6.0232158311e+05
+4.48097e-05 0.117 1.5816877820e+09 3.8739994090e+05
+5.03597e-05 0.117 9.3715407224e+08 2.2745304775e+05
+5.59097e-05 0.117 4.8387917428e+08 1.2101295768e+05
+6.14597e-05 0.117 2.0193586928e+08 6.0055107771e+04
+6.70097e-05 0.117 5.5886110911e+07 3.2749521065e+04
+7.25597e-05 0.117 3.7782348010e+06 2.6350963616e+04
+7.81097e-05 0.117 5.3407817904e+06 2.9624963314e+04
+8.36597e-05 0.117 2.7975485523e+07 3.4403962254e+04
+8.92097e-05 0.117 4.9845448282e+07 3.6130017903e+04
+9.47597e-05 0.117 6.0092588905e+07 3.3495107849e+04
+1.00310e-04 0.117 5.6823430831e+07 2.7475726279e+04
+1.05860e-04 0.117 4.3857024036e+07 2.0144282226e+04
+1.11410e-04 0.117 2.7277144760e+07 1.3647403260e+04
+1.22510e-04 0.117 3.3119334113e+06 6.6519711526e+03
+1.33610e-04 0.117 1.4412859402e+06 6.9726212813e+03
+1.44710e-04 0.117 8.5620162463e+06 8.1441335775e+03
+1.55810e-04 0.117 9.6957429033e+06 6.4559996521e+03
+1.66910e-04 0.117 4.3818341914e+06 3.6252154156e+03
+1.78010e-04 0.117 2.7448997387e+05 2.4006505342e+03
+1.89110e-04 0.117 8.0472009936e+05 2.8187789089e+03
+2.00210e-04 0.117 2.8149552834e+06 3.0915662855e+03
+2.11310e-04 0.117 2.7510907861e+06 2.3722530293e+03
+2.22410e-04 0.117 1.0053133293e+06 1.4473468311e+03
+2.33510e-04 0.117 5.8428305052e+03 1.2048540556e+03
+2.44610e-04 0.117 5.1699305004e+05 1.4625670042e+03
+2.55710e-04 0.117 1.2120227268e+06 1.5010705973e+03
+2.66810e-04 0.117 9.7896842846e+05 1.1336343426e+03
+2.77910e-04 0.117 2.5507264791e+05 8.1848818080e+02
+3.05660e-04 0.117 5.2403101181e+05 7.4913374357e+02
+3.33410e-04 0.117 5.8699343809e+04 4.4669964560e+02
+3.61160e-04 0.117 3.0844327150e+05 4.6774007542e+02
+3.88910e-04 0.117 8.3360142970e+03 2.7169550220e+02
+4.16660e-04 0.117 1.8630080583e+05 3.0710983679e+02
+4.44410e-04 0.117 3.1616804732e-01 1.7959006831e+02
+4.72160e-04 0.117 1.1299016314e+05 2.0763952339e+02
+4.99910e-04 0.117 2.9952522747e+03 1.2536542765e+02
+5.27660e-04 0.117 6.7625695649e+04 1.4013969777e+02
+5.55410e-04 0.117 7.6927460089e+03 8.2145593180e+01
+6.10910e-04 0.117 1.1229057779e+04 8.4519745643e+01
+6.66410e-04 0.117 1.3035567943e+04 8.1554625609e+01
+7.21910e-04 0.117 1.3309931343e+04 7.4437319172e+01
+7.77410e-04 0.117 1.2462626212e+04 6.4697088261e+01
+8.32910e-04 0.117 1.0912927143e+04 5.3773301044e+01
+8.88410e-04 0.117 9.0172597469e+03 4.2843375753e+01
+9.43910e-04 0.117 7.0496495917e+03 3.2771032724e+01
+9.99410e-04 0.117 5.2030483682e+03 2.4113557144e+01
+1.05491e-03 0.117 3.5988976711e+03 1.7160773658e+01
+1.11041e-03 0.117 2.2996060652e+03 1.2016626459e+01
+1.22141e-03 0.117 6.4766590598e+02 6.0373017740e+00
+1.33241e-03 0.117 4.1963483264e+01 4.5215452974e+00
+1.44341e-03 0.117 6.3370708246e+01 5.1054681903e+00
+1.55441e-03 0.117 3.0736750577e+02 5.9176165298e+00
+1.66541e-03 0.117 5.0327682399e+02 5.9815000189e+00
+1.77641e-03 0.117 5.4084331454e+02 5.1634639625e+00
+1.88741e-03 0.117 4.3488671756e+02 3.8535158148e+00
+1.99841e-03 0.117 2.6322287860e+02 2.5824997753e+00
+2.10941e-03 0.117 1.0793633150e+02 1.7315517194e+00
+2.22041e-03 0.117 1.8474448850e+01 1.4077213604e+00
+2.33141e-03 0.117 1.5864062279e+00 1.4771560682e+00
+2.44241e-03 0.117 3.2267213848e+01 1.6916253448e+00
+2.55341e-03 0.117 7.4289116207e+01 1.8274751193e+00
+2.66441e-03 0.117 9.9000521929e+01 1.7706812289e+00
+"""
+
+def main():
+    """
+    Run tests given is sys.argv.
+
+    Returns 0 if success or 1 if any tests fail.
+    """
+    import sys
+    import xmlrunner
+
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]))
+
+    runner = xmlrunner.XMLTestRunner(output='logs')
+    result = runner.run(suite)
+    return 1 if result.failures or result.errors else 0
+
+
+############################################################################
+# usage demo
+############################################################################
+
+def _eval_demo_1d(resolution, title):
+    from sasmodels import core
+    from sasmodels.models import cylinder
+    ## or alternatively:
+    # cylinder = core.load_model_definition('cylinder')
+    model = core.load_model(cylinder)
+
+    kernel = core.make_kernel(model, [resolution.q_calc])
+    theory = core.call_kernel(kernel, {'length':210, 'radius':500})
+    Iq = resolution.apply(theory)
+
+    import matplotlib.pyplot as plt
+    plt.loglog(resolution.q_calc, theory, label='unsmeared')
+    plt.loglog(resolution.q, Iq, label='smeared', hold=True)
+    plt.legend()
+    plt.title(title)
+    plt.xlabel("Q (1/Ang)")
+    plt.ylabel("I(Q) (1/cm)")
+
+def demo_pinhole_1d():
+    q = np.logspace(-3, -1, 400)
+    q_width = 0.1*q
+    resolution = Pinhole1D(q, q_width)
+    _eval_demo_1d(resolution, title="10% dQ/Q Pinhole Resolution")
+
+def demo_slit_1d():
+    q = np.logspace(-3, -1, 400)
+    qx_width = 0.005
+    qy_width = 0.0
+    resolution = Slit1D(q, qx_width, qy_width)
+    _eval_demo_1d(resolution, title="0.005 Qx Slit Resolution")
+
+def demo():
+    import matplotlib.pyplot as plt
+    plt.subplot(121)
+    demo_pinhole_1d()
+    plt.subplot(122)
+    demo_slit_1d()
+    plt.show()
+
+
+if __name__ == "__main__":
+    #demo()
+    main()
Index: src/sas/perspectives/fitting/fitting.py
===================================================================
--- src/sas/perspectives/fitting/fitting.py	(revision f76bf175a3375937e71ee279db24101709d87f13)
+++ src/sas/perspectives/fitting/fitting.py	(revision a3f125f0f3c4ff6ed01529da503fbdd5ccb23b14)
@@ -954,4 +954,5 @@
                 return True
             except:
+                raise
                 msg = "Fitting error: %s" % str(sys.exc_value)
                 wx.PostEvent(self.parent, StatusEvent(status=msg, info="error",
Index: src/sas/perspectives/fitting/model_thread.py
===================================================================
--- src/sas/perspectives/fitting/model_thread.py	(revision 2f4b43041eaa7b7f496750f223a299cb33cd268e)
+++ src/sas/perspectives/fitting/model_thread.py	(revision a3f125f0f3c4ff6ed01529da503fbdd5ccb23b14)
@@ -169,6 +169,6 @@
             first_bin, last_bin = self.smearer.get_bin_range(self.qmin,
                                                              self.qmax)
-            mask = self.data.x[first_bin:last_bin]
-            output[first_bin:last_bin] = self.model.evalDistribution(mask)
+            mask = self.data.x[first_bin:last_bin+1]
+            output[first_bin:last_bin+1] = self.model.evalDistribution(mask)
             output = self.smearer(output, first_bin, last_bin)
         else:
