Index: doc/guide/gpu_setup.rst
===================================================================
--- doc/guide/gpu_setup.rst	(revision 59485a45e308439e03beddcf6e942c0c3e72534b)
+++ doc/guide/gpu_setup.rst	(revision 63602b15f168fcfb37242dc2e94348a4751243a3)
@@ -139,7 +139,7 @@
 the compiler.
 
-On Windows, set *SASCOMPILER=tinycc* for the tinycc compiler,
-*SASCOMPILER=msvc* for the Microsoft Visual C compiler,
-or *SASCOMPILER=mingw* for the MinGW compiler. If TinyCC is available
+On Windows, set *SAS_COMPILER=tinycc* for the tinycc compiler,
+*SAS_COMPILER=msvc* for the Microsoft Visual C compiler,
+or *SAS_COMPILER=mingw* for the MinGW compiler. If TinyCC is available
 on the python path (it is provided with SasView), that will be the
 default. If you want one of the other compilers, be sure to have it
Index: example/model_ellipsoid_hayter_msa.py
===================================================================
--- example/model_ellipsoid_hayter_msa.py	(revision 8a5f0216970a47f3e897a508054840132bd87909)
+++ example/model_ellipsoid_hayter_msa.py	(revision 9a999931f7b7ce4cd7a1ccb4cb1b1c86845e1065)
@@ -16,5 +16,5 @@
 
 # DEFINE THE MODEL
-kernel = load_model('ellipsoid*hayter_msa')
+kernel = load_model('ellipsoid@hayter_msa')
 
 pars = dict(scale=6.4, background=0.06, sld=0.33, sld_solvent=2.15, radius_polar=14.0,
Index: example/slurm_batch.py
===================================================================
--- example/slurm_batch.py	(revision 1d9998c981957f42dd41ba7220fbc68bdfba7539)
+++ example/slurm_batch.py	(revision 1d9998c981957f42dd41ba7220fbc68bdfba7539)
@@ -0,0 +1,241 @@
+#!/usr/bin/env python
+"""
+Submit a batch fit job to the slurm cluster.
+
+Given a model.py file defining a Bumps problem defined on a single data
+file, with the data file specified as a command line argument, run the
+bumps fit as a batch over a set of different datafiles independently.
+An example model is given in model_ellipsoid_hayter_msa.py, which fits 
+the data in 09319*.dat.
+
+To run the fit, use::
+
+    slurm_batch.py [--slurm_opts] model.py *.dat --store=T1 [--bumps_opt ...]
+
+For example::
+
+    slurm_batch.py model_ellipsoid_hayter_msa.py 09319*.dat --store=T1
+
+This creates the T1 subdirectory to hold the fit results and 
+prints the real command that is submitted, as well as the job id.
+
+The store directory T1 contains a copy of the model file and 
+all the data files.  The fit results for each file will be 
+in T1/##/*.  The file T1/files.dat contains the list 
+of "subdirectory filename" pairs indicating which ## directory 
+contains the resuls for which file.  Check for errors using::
+
+    cat T1/slurm*_1.out
+
+The following slurm options are used::
+
+    --array=1-#files     batch size comes from the file list
+    --gres=gpu:1         request a gpu for each fit
+    --job-name=model.py  use model file name for job name
+    --output=...         log into T1/slurm-job_##.out
+    --chdir=...          run fit from store directory
+    --time=2             time as number of hours (can override)
+
+To receive an email on job completion or failure, add the following
+slurm options before the model file::
+
+    --mail-type=END,FAIL --mail-user=user@mail.domain
+
+Bumps options are described at bumps.readthedocs.org, with the
+following set automatically::
+
+    --batch              run in batch mode, without output to .mon
+    --view=log           SAS fits want log plots
+    --time=2-0.1         slurm time minus 6 minutes for cleanup
+
+The --store and --resume options indicate the parent directory for
+the output.  These are modified to store the results in a separate
+subdirectory for each file.  Keep in mind that the fit is run from
+the store directory, so any files or modules referenced from the
+model file will need to use a full path to the original location.
+
+After submitting the job a job id will be printed to the console.
+You can check the status of the job using the usual slurm commands
+such as::
+
+    squeue
+
+or cancel the job using::
+
+    scancel jobid
+
+The slurm_batch program runs directly from the source tree for sasmodels, 
+and requires sasview, bumps and periodictable as sister directories
+accessible on the worker nodes.  You can link it into your bin directory
+using::
+
+     mkdir ~/bin
+     ln -s path/to/slurm_batch.py ~/bin
+
+or if you are a cluster administrator, into /usr/local/bin.
+"""
+
+# If called from command line, this submits a job to the slurm queue, with _this_ file
+# as the batch script. Before calling it on the worker node, slurm sets the
+# SLURM_ARRAY_TASK_ID to the current task so we can tell that we are running
+# as a worker and which file we should be working on.
+
+## SBATCH options as comments do not seem to work.  Maybe they neeed to be before 
+## the doc string?  For now they are hardcoded in the sbatch call in submit_job.
+
+import sys
+import os
+import tempfile
+import shutil
+
+DEFAULT_TIME_LIMIT = 2
+
+def split_args():
+    slurm_opts = []
+    bumps_opts = []
+    model_file = None
+    store = None
+    resume = None
+    data_files = []
+    time_limit = DEFAULT_TIME_LIMIT
+
+    # start with '-' arguments as slurm opts, then after
+    # the model file any '-' arguments are bumps opts.
+    opts = slurm_opts
+    for v in sys.argv[1:]:
+        if v.startswith('--store='):
+            store = os.path.realpath(os.path.abspath(v[8:]))
+        elif v.startswith('--resume='):
+            resume = os.path.realpath(os.path.abspath(v[9:]))
+        elif v.startswith('--time='):
+            time_limit = float(v[7:])
+        elif v[0] == '-':
+            opts.append(v)
+        elif model_file is None:
+            model_file = v
+            opts = bumps_opts
+        else:
+            data_files.append(v)
+
+
+    s = time_limit*3600
+    slurm_opts.append("--time=%d:%02d:%02d"%(s//3600, (s%3600)//60, s%60))
+    bumps_opts.append('--time=%f'%(time_limit - 0.1))  # 6 min to stop cleanly
+
+    return {
+        'slurm': slurm_opts, 
+        'model_file': model_file, 
+        'data_files': data_files, 
+        'store': store, 
+        'resume': resume,
+        'bumps': bumps_opts,
+    }
+
+def dirn(path, n):
+    path = os.path.realpath(os.path.abspath(path))
+    for _ in range(n):
+        path = os.path.dirname(path)
+    return path
+
+def submit_job():
+    # sbatch --array=1-5 ./slurm_batch.py model_ellipsoid_hayter_msa.py 09*.dat --store=T1 --fit=dream
+    opts = split_args()
+    store = opts['store']
+    model_file = opts['model_file']
+    data_files = opts['data_files']
+    bumps_opts = opts['bumps']
+    slurm_opts = opts['slurm']
+
+    # make sure the store directory exists and save the order of the files, as well
+    # as the model and the data files
+    if store is not None:
+        if not os.path.exists(store):
+            os.makedirs(store)
+
+        # save file order
+        with open(os.path.join(store, 'files.dat'), 'w') as fid:
+            for k, f in enumerate(data_files):
+                fid.write("%02d %s\n"%(k+1, f))
+
+        # Copy the model and data files to the root store directory
+        # Since bumps changes into the model directory prior to loading
+        # the datafiles, strip all leading paths from data and model and
+        # set the working directory for the job to the store directory.
+        model_copy = os.path.basename(model_file)
+        shutil.copy(model_file, os.path.join(store, model_copy))
+        data_copy = []
+        for f in data_files:
+            f_copy = os.path.basename(f)
+            shutil.copy(f, os.path.join(store, f_copy))
+            data_copy.append(f_copy) 
+
+        model_file = model_copy
+        data_files = data_copy
+
+
+    # build and run the command
+    SRC = dirn(__file__, 3) # __file__ is $SRC/sasmodels/example/slurm_batch.py
+    parts = [
+        "sbatch",
+        "--array=1-%d"%len(data_files),
+        "--gres=gpu:1",
+        "--job-name="+model_file,
+        ## since we are setting the current working directory, we don't need
+        ## to fiddle the slurm output files
+        "--output=%s/slurm-%%A_%%a.out"%store,
+        "--chdir=%s"%store,
+        ]
+    parts.extend(slurm_opts)
+    parts.append(__file__)
+    # Remember the source root so we can reconstruct the correct python path
+    # This is done after the model file so that it doesn't get interpreted
+    # as a slurm option.
+    parts.append("--source_root=%s"%SRC)
+    parts.append(model_file)
+    parts.extend(data_files)
+    parts.extend(bumps_opts)
+    #if store is not None:
+    #    parts.append("--store=" + store)
+    command = " ".join(parts)
+
+    print(command)
+    os.system(command)
+
+def run_task(task_id):
+    opts = split_args()
+
+    # Set environment put compiled sasmodels in user-specific temporary cache
+    # We need this because users don't have a home directory on the individual
+    # cluster nodes.
+    assert opts['slurm'][0].startswith('--source_root=')
+    SRC = opts['slurm'][0][14:]
+    PACKAGES = ("periodictable", "sasview/src", "bumps", "sasmodels")
+    os.environ['PYTHONPATH'] = ":".join(SRC+"/"+v for v in PACKAGES)
+    TMP = tempfile.gettempdir()
+    cache_path = os.path.join(TMP, os.environ['USER'], '.cache')
+    os.environ['SAS_DLL_PATH'] = cache_path
+    os.environ['XDG_CACHE_HOME'] = cache_path
+
+    #task_store = "%s/%02d"%(opts['store'], task_id)
+    task_store = "%02d"%task_id
+    parts = [
+       "python", os.path.join(SRC, "bumps", "run.py"), "--batch",
+       "--view=log",
+       opts['model_file'],
+       opts['data_files'][task_id-1],
+       ]
+    parts.extend(opts['bumps'])
+    parts.append('--store='+task_store)
+    if opts['resume'] is not None:
+        parts.append('--resume='+os.path.join(opts['resume'], task_store))
+    command = " ".join(parts)
+    print(os.getcwd() + "$ " + command)
+    os.system(command)
+
+
+task_id = int(os.environ.get('SLURM_ARRAY_TASK_ID', -1))
+if task_id == -1:
+    submit_job()
+else:
+    run_task(task_id)
+
Index: sasmodels/compare.py
===================================================================
--- sasmodels/compare.py	(revision 1e7b202a912b85e208919f52c320e14231e93bc8)
+++ sasmodels/compare.py	(revision af7a97c2acc05ec1c5d537fa21912dab44ff661a)
@@ -112,4 +112,12 @@
     -edit starts the parameter explorer
     -help/-html shows the model docs instead of running the model
+
+    === environment variables ===
+    -DSAS_MODELPATH=path sets directory containing custom models
+    -DSAS_OPENCL=vendor:device|none sets the target OpenCL device
+    -DXDG_CACHE_HOME=~/.cache sets the pyopencl cache root (linux only)
+    -DSAS_COMPILER=tinycc|msvc|mingw|unix sets the DLL compiler
+    -DSAS_OPENMP=1 turns on OpenMP for the DLLs
+    -DSAS_DLL_PATH=path sets the path to the compiled modules
 
 The interpretation of quad precision depends on architecture, and may
@@ -1113,6 +1121,7 @@
 
     invalid = [o[1:] for o in flags
-               if o[1:] not in NAME_OPTIONS
-               and not any(o.startswith('-%s='%t) for t in VALUE_OPTIONS)]
+               if not (o[1:] in NAME_OPTIONS
+                       or any(o.startswith('-%s='%t) for t in VALUE_OPTIONS)
+                       or o.startswith('-D'))]
     if invalid:
         print("Invalid options: %s"%(", ".join(invalid)))
@@ -1215,4 +1224,7 @@
         elif arg == '-html':    opts['html'] = True
         elif arg == '-help':    opts['html'] = True
+        elif arg.startswith('-D'):
+            var, val = arg[2:].split('=')
+            os.environ[var] = val
     # pylint: enable=bad-whitespace,C0321
 
Index: sasmodels/core.py
===================================================================
--- sasmodels/core.py	(revision c036ddbd6dd028e64cda5692721d394c9845adaf)
+++ sasmodels/core.py	(revision c036ddbd6dd028e64cda5692721d394c9845adaf)
@@ -37,6 +37,6 @@
 if CUSTOM_MODEL_PATH == "":
     CUSTOM_MODEL_PATH = joinpath(os.path.expanduser("~"), ".sasmodels", "custom_models")
-    if not os.path.isdir(CUSTOM_MODEL_PATH):
-        os.makedirs(CUSTOM_MODEL_PATH)
+    #if not os.path.isdir(CUSTOM_MODEL_PATH):
+    #    os.makedirs(CUSTOM_MODEL_PATH)
 
 # TODO: refactor composite model support
@@ -232,10 +232,10 @@
         if not callable(model_info.Iq):
             source = generate.make_source(model_info)['dll']
-            old_path = kerneldll.DLL_PATH
+            old_path = kerneldll.SAS_DLL_PATH
             try:
-                kerneldll.DLL_PATH = path
+                kerneldll.SAS_DLL_PATH = path
                 dll = kerneldll.make_dll(source, model_info, dtype=numpy_dtype)
             finally:
-                kerneldll.DLL_PATH = old_path
+                kerneldll.SAS_DLL_PATH = old_path
             compiled_dlls.append(dll)
     return compiled_dlls
Index: sasmodels/data.py
===================================================================
--- sasmodels/data.py	(revision c036ddbd6dd028e64cda5692721d394c9845adaf)
+++ sasmodels/data.py	(revision c036ddbd6dd028e64cda5692721d394c9845adaf)
@@ -502,9 +502,11 @@
             # Note: masks merge, so any masked theory points will stay masked,
             # and the data mask will be added to it.
-            mtheory = masked_array(theory, data.mask.copy())
+            #mtheory = masked_array(theory, data.mask.copy())
+            theory_x = data.x[data.mask == 0]
+            mtheory = masked_array(theory)
             mtheory[~np.isfinite(mtheory)] = masked
             if view is 'log':
                 mtheory[mtheory <= 0] = masked
-            plt.plot(data.x, scale*mtheory, '-')
+            plt.plot(theory_x, scale*mtheory, '-')
             all_positive = all_positive and (mtheory > 0).all()
             some_present = some_present or (mtheory.count() > 0)
@@ -542,5 +544,6 @@
 
     if use_resid:
-        mresid = masked_array(resid, data.mask.copy())
+        theory_x = data.x[data.mask == 0]
+        mresid = masked_array(resid)
         mresid[~np.isfinite(mresid)] = masked
         some_present = (mresid.count() > 0)
@@ -548,5 +551,5 @@
         if num_plots > 1:
             plt.subplot(1, num_plots, use_calc + 2)
-        plt.plot(data.x, mresid, '.')
+        plt.plot(theory_x, mresid, '.')
         plt.xlabel("$q$/A$^{-1}$")
         plt.ylabel('residuals')
Index: sasmodels/direct_model.py
===================================================================
--- sasmodels/direct_model.py	(revision d18d6ddf640c8cff5c76cfac900c1940989612ca)
+++ sasmodels/direct_model.py	(revision 1a8c11c9d9d7223aecf2c56d6742e33302bffda3)
@@ -250,5 +250,5 @@
             qmax = getattr(data, 'qmax', np.inf)
             accuracy = getattr(data, 'accuracy', 'Low')
-            index = ~data.mask & (q >= qmin) & (q <= qmax)
+            index = (data.mask == 0) & (q >= qmin) & (q <= qmax)
             if data.data is not None:
                 index &= ~np.isnan(data.data)
@@ -263,4 +263,7 @@
         elif self.data_type == 'Iq':
             index = (data.x >= data.qmin) & (data.x <= data.qmax)
+            mask = getattr(data, 'mask', None)
+            if mask is not None:
+                index &= (mask == 0)
             if data.y is not None:
                 index &= ~np.isnan(data.y)
Index: sasmodels/kerneldll.py
===================================================================
--- sasmodels/kerneldll.py	(revision c036ddbd6dd028e64cda5692721d394c9845adaf)
+++ sasmodels/kerneldll.py	(revision c036ddbd6dd028e64cda5692721d394c9845adaf)
@@ -99,4 +99,10 @@
     pass
 # pylint: enable=unused-import
+
+if "SAS_DLL_PATH" in os.environ:
+    SAS_DLL_PATH = os.environ["SAS_DLL_PATH"]
+else:
+    # Assume the default location of module DLLs is in .sasmodels/compiled_models.
+    SAS_DLL_PATH = os.path.join(os.path.expanduser("~"), ".sasmodels", "compiled_models")
 
 if "SAS_COMPILER" in os.environ:
@@ -161,7 +167,4 @@
         return CC + [source, "-o", output, "-lm"]
 
-# Assume the default location of module DLLs is in .sasmodels/compiled_models.
-DLL_PATH = os.path.join(os.path.expanduser("~"), ".sasmodels", "compiled_models")
-
 ALLOW_SINGLE_PRECISION_DLLS = True
 
@@ -200,5 +203,5 @@
         return path
 
-    return joinpath(DLL_PATH, basename)
+    return joinpath(SAS_DLL_PATH, basename)
 
 
@@ -209,5 +212,5 @@
     exist yet if it hasn't been compiled.
     """
-    return os.path.join(DLL_PATH, dll_name(model_info, dtype))
+    return os.path.join(SAS_DLL_PATH, dll_name(model_info, dtype))
 
 
@@ -228,5 +231,6 @@
     models are not allowed as DLLs.
 
-    Set *sasmodels.kerneldll.DLL_PATH* to the compiled dll output path.
+    Set *sasmodels.kerneldll.SAS_DLL_PATH* to the compiled dll output path.
+    Alternatively, set the environment variable *SAS_DLL_PATH*.
     The default is in ~/.sasmodels/compiled_models.
     """
@@ -247,6 +251,6 @@
     if need_recompile:
         # Make sure the DLL path exists
-        if not os.path.exists(DLL_PATH):
-            os.makedirs(DLL_PATH)
+        if not os.path.exists(SAS_DLL_PATH):
+            os.makedirs(SAS_DLL_PATH)
         basename = splitext(os.path.basename(dll))[0] + "_"
         system_fd, filename = tempfile.mkstemp(suffix=".c", prefix=basename)
Index: sasmodels/model_test.py
===================================================================
--- sasmodels/model_test.py	(revision 3221de0ed9b3480163246ceb0503432126bd5134)
+++ sasmodels/model_test.py	(revision 012cd342ba6d27483147e3401f01fb7b93ea589c)
@@ -376,12 +376,4 @@
         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)
 
     # Warn if there are no user defined tests.
@@ -393,4 +385,7 @@
     # iterator since we don't have direct access to the list of tests in the
     # test suite.
+    # In Qt5 suite.run() will clear all tests in the suite after running
+    # with no way of retaining them for the test below, so let's check
+    # for user tests before running the suite.
     for test in suite:
         if not test.info.tests:
@@ -399,4 +394,13 @@
     else:
         stream.writeln("Note: no test suite created --- this should never happen")
+
+    # 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)
 
     output = stream.getvalue()
Index: sasmodels/modelinfo.py
===================================================================
--- sasmodels/modelinfo.py	(revision c036ddbd6dd028e64cda5692721d394c9845adaf)
+++ sasmodels/modelinfo.py	(revision c036ddbd6dd028e64cda5692721d394c9845adaf)
@@ -585,5 +585,5 @@
                 Parameter('up:frac_f', '', 0., [0., 1.],
                           'magnetic', 'fraction of spin up final'),
-                Parameter('up:angle', 'degress', 0., [0., 360.],
+                Parameter('up:angle', 'degrees', 0., [0., 360.],
                           'magnetic', 'spin up angle'),
             ])
Index: sasmodels/models/guinier.py
===================================================================
--- sasmodels/models/guinier.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/models/guinier.py	(revision c9fc87335cd86f10c8c9e3654927fdeeba6a70af)
@@ -7,5 +7,5 @@
 .. math::
 
-    I(q) = \text{scale} \cdot \exp{\left[ \frac{-Q^2R_g^2}{3} \right]}
+    I(q) = \text{scale} \cdot \exp{\left[ \frac{-Q^2 R_g^2 }{3} \right]}
             + \text{background}
 
@@ -19,4 +19,27 @@
 
 .. math:: q = \sqrt{q_x^2 + q_y^2}
+
+In scattering, the radius of gyration $R_g$ quantifies the objects's
+distribution of SLD (not mass density, as in mechanics) from the objects's
+SLD centre of mass. It is defined by
+
+.. math:: R_g^2 = \frac{\sum_i\rho_i\left(r_i-r_0\right)^2}{\sum_i\rho_i}
+
+where $r_0$ denotes the object's SLD centre of mass and $\rho_i$ is the SLD at
+a point $i$.
+
+Notice that $R_g^2$ may be negative (since SLD can be negative), which happens
+when a form factor $P(Q)$ is increasing with $Q$ rather than decreasing. This
+can occur for core/shell particles, hollow particles, or for composite
+particles with domains of different SLDs in a solvent with an SLD close to the
+average match point. (Alternatively, this might be regarded as there being an
+internal inter-domain "structure factor" within a single particle which gives
+rise to a peak in the scattering).
+
+To specify a negative value of $R_g^2$ in SasView, simply give $R_g$ a negative
+value ($R_g^2$ will be evaluated as $R_g |R_g|$). Note that the physical radius 
+of gyration, of the exterior of the particle, will still be large and positive. 
+It is only the apparent size from the small $Q$ data that will give a small or 
+negative value of $R_g^2$.
 
 References
@@ -42,8 +65,8 @@
 
 #             ["name", "units", default, [lower, upper], "type","description"],
-parameters = [["rg", "Ang", 60.0, [0, inf], "", "Radius of Gyration"]]
+parameters = [["rg", "Ang", 60.0, [-inf, inf], "", "Radius of Gyration"]]
 
 Iq = """
-    double exponent = rg*rg*q*q/3.0;
+    double exponent = fabs(rg)*rg*q*q/3.0;
     double value = exp(-exponent);
     return value;
@@ -66,5 +89,5 @@
 
 # parameters for demo
-demo = dict(scale=1.0, rg=60.0)
+demo = dict(scale=1.0,  background=0.001, rg=60.0 )
 
 # parameters for unit tests
Index: sasmodels/resolution.py
===================================================================
--- sasmodels/resolution.py	(revision 0b9c6df46513d1c375b80d4d720164332391302a)
+++ sasmodels/resolution.py	(revision 9e7837a27701a2e9059daf0ed4b8c5127a10bf59)
@@ -20,5 +20,8 @@
 MINIMUM_RESOLUTION = 1e-8
 MINIMUM_ABSOLUTE_Q = 0.02  # relative to the minimum q in the data
-PINHOLE_N_SIGMA = 2.5 # From: Barker & Pedersen 1995 JAC
+# According to (Barker & Pedersen 1995 JAC), 2.5 sigma is a good limit.
+# According to simulations with github.com:scattering/sansresolution.git
+# it is better to use asymmetric bounds (2.5, 3.0)
+PINHOLE_N_SIGMA = (2.5, 3.0)
 
 class Resolution(object):
@@ -90,6 +93,6 @@
         # from the geometry, they may appear since we are using a truncated
         # gaussian to represent resolution rather than a skew distribution.
-        cutoff = MINIMUM_ABSOLUTE_Q*np.min(self.q)
-        self.q_calc = self.q_calc[self.q_calc >= cutoff]
+        #cutoff = MINIMUM_ABSOLUTE_Q*np.min(self.q)
+        #self.q_calc = self.q_calc[self.q_calc >= cutoff]
 
         # Build weight matrix from calculated q values
@@ -188,8 +191,12 @@
     cdf = erf((edges[:, None] - q[None, :]) / (sqrt(2.0)*q_width)[None, :])
     weights = cdf[1:] - cdf[:-1]
-    # Limit q range to +/- 2.5 sigma
-    qhigh = q + nsigma*q_width
-    #qlow = q - nsigma*q_width  # linear limits
-    qlow = q*q/qhigh  # log limits
+    # Limit q range to (-2.5,+3) sigma
+    try:
+        nsigma_low, nsigma_high = nsigma
+    except TypeError:
+        nsigma_low = nsigma_high = nsigma
+    qhigh = q + nsigma_high*q_width
+    qlow = q - nsigma_low*q_width  # linear limits
+    ##qlow = q*q/qhigh  # log limits
     weights[q_calc[:, None] < qlow[None, :]] = 0.
     weights[q_calc[:, None] > qhigh[None, :]] = 0.
@@ -365,5 +372,5 @@
 
 
-def pinhole_extend_q(q, q_width, nsigma=3):
+def pinhole_extend_q(q, q_width, nsigma=PINHOLE_N_SIGMA):
     """
     Given *q* and *q_width*, find a set of sampling points *q_calc* so
@@ -371,5 +378,9 @@
     function.
     """
-    q_min, q_max = np.min(q - nsigma*q_width), np.max(q + nsigma*q_width)
+    try:
+        nsigma_low, nsigma_high = nsigma
+    except TypeError:
+        nsigma_low = nsigma_high = nsigma
+    q_min, q_max = np.min(q - nsigma_low*q_width), np.max(q + nsigma_high*q_width)
     return linear_extrapolation(q, q_min, q_max)
 
