Index: .gitignore
===================================================================
--- .gitignore	(revision 153b75a64d88ad8b0597c08dada6ae0ca20c08d1)
+++ .gitignore	(revision 3fca52c4f75eb23b57cb047514c6a35696eea2b6)
@@ -39,4 +39,5 @@
 
 # test outputs
+/.eggs
 /test/pr_inversion/test/test_output.txt
 /test/sasdataloader/test/plugins.zip
Index: conftest.py
===================================================================
--- conftest.py	(revision 3fca52c4f75eb23b57cb047514c6a35696eea2b6)
+++ conftest.py	(revision 3fca52c4f75eb23b57cb047514c6a35696eea2b6)
@@ -0,0 +1,7 @@
+"""
+Pytest configuration file.
+"""
+
+# Prepare the runtime environment for the tests
+import run
+run.prepare()
Index: fit.py
===================================================================
--- fit.py	(revision a072198718360f92067da8af1514b709b3838426)
+++ fit.py	(revision a072198718360f92067da8af1514b709b3838426)
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+"""
+Bumps model file for running saved sasview fits.
+
+Fit configuration (including fitted variables and ranges) is set up in the
+Sasview GUI and saved as a .svs or .fitv file.  These can then be run
+directly from bumps using::
+
+    path/to/sasview/fit.py path/to/save.svs [--bumps_option ...]
+
+Alternatively, if sasview is installed on your python path, use::
+
+    python -m sas.sascalc.fit.fitstate path/to/save.svs [--bumps_option ...]
+
+In unix/mac you can create a file on your shell path (e.g., ~/bin/sasbumps)
+containing::
+
+    #!/usr/bin/env python
+    from sas.sascalc.fit.fitstate import bumps_cli
+    bumps_cli()
+
+set it as executable with::
+
+    chmod a+x ~/bin/sasbumps
+
+and use::
+
+    sasbumps path/to/save.svs [--bumps_option ...]
+"""
+
+# ============ Set up python environment ==============
+import sys
+import os
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from run import prepare
+del sys.path[0]
+prepare(rebuild=False)
+
+# =========== Set up logging ===========
+#from sas.logger_config import SetupLogger
+#SetupLogger(__name__).config_development()
+
+# =========== Run BUMPS ===============
+from sas.sascalc.fit.fitstate import bumps_cli
+bumps_cli()
Index: setup.py
===================================================================
--- setup.py	(revision 952ea1fd0d931d2eae09831204ea057abfe5450f)
+++ setup.py	(revision 46119c2f4dbe06ef0ff27514ccf0b4cbc4493762)
@@ -55,23 +55,4 @@
 CURRENT_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
 SASVIEW_BUILD = os.path.join(CURRENT_SCRIPT_DIR, "build")
-
-# TODO: build step should not be messing with existing installation!!
-sas_dir = os.path.join(os.path.expanduser("~"), '.sasview')
-if os.path.isdir(sas_dir):
-    f_path = os.path.join(sas_dir, "sasview.log")
-    if os.path.isfile(f_path):
-        os.remove(f_path)
-    f_path = os.path.join(sas_dir, "categories.json")
-    if os.path.isfile(f_path):
-        os.remove(f_path)
-    f_path = os.path.join(sas_dir, 'config', "custom_config.py")
-    if os.path.isfile(f_path):
-        os.remove(f_path)
-    #f_path = os.path.join(sas_dir, 'plugin_models')
-    # if os.path.isdir(f_path):
-    #     for f in os.listdir(f_path):
-    #         if f in plugin_model_list:
-    #             file_path =  os.path.join(f_path, f)
-    #             os.remove(file_path)
 
 
Index: src/sas/sascalc/calculator/instrument.py
===================================================================
--- src/sas/sascalc/calculator/instrument.py	(revision f4775563fe889a4e12295a145627987e7a33ab93)
+++ src/sas/sascalc/calculator/instrument.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -128,5 +128,9 @@
             self.size = 0
         else:
-            self.size = size
+            # TODO: Make sure detector size is number of pixels
+            # Could be detector dimensions in e.g., mm, but
+            # the resolution calculator assumes it is pixels.
+            # Being pixels, it has to be integers rather than float
+            self.size = [int(s) for s in size]
             validate(size[0])
 
Index: src/sas/sascalc/calculator/resolution_calculator.py
===================================================================
--- src/sas/sascalc/calculator/resolution_calculator.py	(revision 574adc767a9e86744546f8edd6a23dc4f81914ad)
+++ src/sas/sascalc/calculator/resolution_calculator.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -1007,6 +1007,6 @@
         try:
             detector_offset = self.sample2detector_distance[1]
-        except:
-            logger.error(sys.exc_value)
+        except Exception as exc:
+            logger.error(exc)
 
         # detector size in [no of pix_x,no of pix_y]
@@ -1094,6 +1094,6 @@
             output.qx_data = qx_value
             output.qy_data = qy_value
-        except:
-            logger.error(sys.exc_value)
+        except Exception as exc:
+            logger.error(exc)
 
         return output
Index: src/sas/sascalc/corfunc/corfunc_calculator.py
===================================================================
--- src/sas/sascalc/corfunc/corfunc_calculator.py	(revision a26f67f1457e7e39b79d4c3800c03bcaa208dff7)
+++ src/sas/sascalc/corfunc/corfunc_calculator.py	(revision dbfd307df25dbe7befe02b50ded9d06ccf6499e3)
@@ -30,11 +30,11 @@
             self.start = start
             self.stop = stop
-            self._lastx = []
-            self._lasty = []
+            self._lastx = np.empty(0, dtype='d')
+            self._lasty = None
 
         def __call__(self, x):
             # If input is a single number, evaluate the function at that number
             # and return a single number
-            if type(x) == float or type(x) == int:
+            if isinstance(x, (float, int)):
                 return self._smoothed_function(np.array([x]))[0]
             # If input is a list, and is different to the last input, evaluate
@@ -42,7 +42,6 @@
             # the function was called, return the result that was calculated
             # last time instead of explicity evaluating the function again.
-            elif self._lastx == [] or x.tolist() != self._lastx.tolist():
-                self._lasty = self._smoothed_function(x)
-                self._lastx = x
+            if not np.array_equal(x, self._lastx):
+                self._lastx, self._lasty = x, self._smoothed_function(x)
             return self._lasty
 
@@ -88,5 +87,5 @@
         # Only process data of the class Data1D
         if not issubclass(data.__class__, Data1D):
-            raise ValueError("Data must be of the type DataLoader.Data1D")
+            raise ValueError("Correlation function cannot be computed with 2D data.")
 
         # Prepare the data
@@ -246,5 +245,7 @@
         """Fit the Guinier region of the curve"""
         A = np.vstack([q**2, np.ones(q.shape)]).T
-        return lstsq(A, np.log(iq))
+        # CRUFT: numpy>=1.14.0 allows rcond=None for the following default
+        rcond = np.finfo(float).eps * max(A.shape)
+        return lstsq(A, np.log(iq), rcond=rcond)
 
     def _fit_porod(self, q, iq):
Index: src/sas/sascalc/data_util/calcthread.py
===================================================================
--- src/sas/sascalc/data_util/calcthread.py	(revision 574adc767a9e86744546f8edd6a23dc4f81914ad)
+++ src/sas/sascalc/data_util/calcthread.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -6,27 +6,22 @@
 from __future__ import print_function
 
-import traceback
 import sys
 import logging
+import traceback
+from time import sleep
+
 try:
     import _thread as thread
 except ImportError: # CRUFT: python 2 support
     import thread
-
-if sys.platform.count("darwin") > 0:
-    import time
-    stime = time.time()
-
-    def clock():
-        return time.time() - stime
-
-    def sleep(t):
-        return time.sleep(t)
-else:
-    from time import clock
-    from time import sleep
+try:
+    from time import perf_counter as clock
+except ImportError: # CRUFT: python < 3.3
+    if sys.platform.count("darwin") > 0:
+        from time import time as clock
+    else:
+        from time import clock
 
 logger = logging.getLogger(__name__)
-
 
 class CalcThread:
@@ -248,5 +243,5 @@
                 self.exception_handler(*sys.exc_info())
                 return
-            except Exception:
+            except Exception as exc:
                 pass
         logger.error(traceback.format_exc())
Index: src/sas/sascalc/data_util/nxsunit.py
===================================================================
--- src/sas/sascalc/data_util/nxsunit.py	(revision b011ecbdc80ebea6a89f794bb503745e7f6e5785)
+++ src/sas/sascalc/data_util/nxsunit.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -99,9 +99,8 @@
     Strip '^' from unit names.
 
-    * WARNING * this will incorrect transform 10^3 to 103.
-    """
-    s.update((k.replace('^',''),v)
-             for k, v in list(s.items())
-             if '^' in k)
+    * WARNING * this will incorrectly transform 10^3 to 103.
+    """
+    stripped = [(k.replace('^',''),v) for k, v in s.items() if '^' in k]
+    s.update(stripped)
 
 def _build_all_units():
Index: src/sas/sascalc/data_util/ordereddicttest.py
===================================================================
--- src/sas/sascalc/data_util/ordereddicttest.py	(revision b699768550de9d4b35b087234ec1d2a461922bab)
+++ src/sas/sascalc/data_util/ordereddicttest.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -147,8 +147,8 @@
                     ]):
             self.assert_(dup is not od)
-            self.assertEquals(dup, od)
-            self.assertEquals(list(dup.items()), list(od.items()))
-            self.assertEquals(len(dup), len(od))
-            self.assertEquals(type(dup), type(od))
+            self.assertEqual(dup, od)
+            self.assertEqual(list(dup.items()), list(od.items()))
+            self.assertEqual(len(dup), len(od))
+            self.assertEqual(type(dup), type(od))
 
     def test_repr(self):
Index: src/sas/sascalc/data_util/registry.py
===================================================================
--- src/sas/sascalc/data_util/registry.py	(revision a26f67f1457e7e39b79d4c3800c03bcaa208dff7)
+++ src/sas/sascalc/data_util/registry.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -148,3 +148,3 @@
             # If file has associated loader(s) and they;ve failed
             raise last_exc
-        raise NoKnownLoaderException(e.message)  # raise generic exception
+        raise NoKnownLoaderException(str(message))  # raise generic exception
Index: src/sas/sascalc/data_util/uncertainty.py
===================================================================
--- src/sas/sascalc/data_util/uncertainty.py	(revision 574adc767a9e86744546f8edd6a23dc4f81914ad)
+++ src/sas/sascalc/data_util/uncertainty.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -19,5 +19,5 @@
 import numpy as np
 
-from .import err1d
+from . import err1d
 from .formatnum import format_uncertainty
 
Index: src/sas/sascalc/dataloader/data_info.py
===================================================================
--- src/sas/sascalc/dataloader/data_info.py	(revision 4fdcc659bc3efafa4310cdbe335d80ef80d1b7bb)
+++ src/sas/sascalc/dataloader/data_info.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -26,4 +26,5 @@
 import numpy as np
 import math
+from math import fabs
 
 class plottable_1D(object):
@@ -656,5 +657,5 @@
         return self._perform_operation(other, operation)
 
-    def __div__(self, other):
+    def __truediv__(self, other):
         """
         Divided a data set by another
@@ -667,6 +668,7 @@
             return a/b
         return self._perform_operation(other, operation)
-
-    def __rdiv__(self, other):
+    __div__ = __truediv__
+
+    def __rtruediv__(self, other):
         """
         Divided a data set by another
@@ -679,4 +681,5 @@
             return b/a
         return self._perform_operation(other, operation)
+    __rdiv__ = __rtruediv__
 
     def __or__(self, other):
@@ -800,5 +803,5 @@
             TOLERANCE = 0.01
             for i in range(len(self.x)):
-                if math.fabs((self.x[i] - other.x[i])/self.x[i]) > TOLERANCE:
+                if fabs(self.x[i] - other.x[i]) > self.x[i]*TOLERANCE:
                     msg = "Incompatible data sets: x-values do not match"
                     raise ValueError(msg)
@@ -1032,8 +1035,8 @@
                 raise ValueError(msg)
             for ind in range(len(self.data)):
-                if math.fabs((self.qx_data[ind] - other.qx_data[ind])/self.qx_data[ind]) > TOLERANCE:
+                if fabs(self.qx_data[ind] - other.qx_data[ind]) > fabs(self.qx_data[ind])*TOLERANCE:
                     msg = "Incompatible data sets: qx-values do not match: %s %s" % (self.qx_data[ind], other.qx_data[ind])
                     raise ValueError(msg)
-                if math.fabs((self.qy_data[ind] - other.qy_data[ind])/self.qy_data[ind]) > TOLERANCE:
+                if fabs(self.qy_data[ind] - other.qy_data[ind]) > fabs(self.qy_data[ind])*TOLERANCE:
                     msg = "Incompatible data sets: qy-values do not match: %s %s" % (self.qy_data[ind], other.qy_data[ind])
                     raise ValueError(msg)
Index: src/sas/sascalc/dataloader/loader.py
===================================================================
--- src/sas/sascalc/dataloader/loader.py	(revision b1ec23da8185a2a0dbda947ce190774e0b87aa90)
+++ src/sas/sascalc/dataloader/loader.py	(revision b1ec23da8185a2a0dbda947ce190774e0b87aa90)
@@ -169,7 +169,7 @@
                         if self._identify_plugin(module):
                             readers_found += 1
-                    except:
+                    except Exception as exc:
                         msg = "Loader: Error importing "
-                        msg += "%s\n  %s" % (item, sys.exc_value)
+                        msg += "%s\n  %s" % (item, exc)
                         logger.error(msg)
 
@@ -191,12 +191,12 @@
                                 if self._identify_plugin(module):
                                     readers_found += 1
-                            except:
+                            except Exception as exc:
                                 msg = "Loader: Error importing"
-                                msg += " %s\n  %s" % (mfile, sys.exc_value)
+                                msg += " %s\n  %s" % (mfile, exc)
                                 logger.error(msg)
 
-                    except:
+                    except Exception as exc:
                         msg = "Loader: Error importing "
-                        msg += " %s\n  %s" % (item, sys.exc_value)
+                        msg += " %s\n  %s" % (item, exc)
                         logger.error(msg)
 
@@ -242,7 +242,7 @@
                     self.writers[ext].append(loader.write)
 
-            except:
+            except Exception as exc:
                 msg = "Loader: Error accessing"
-                msg += " Reader in %s\n  %s" % (module.__name__, sys.exc_value)
+                msg += " Reader in %s\n  %s" % (module.__name__, exc)
                 logger.error(msg)
         return reader_found
@@ -275,7 +275,7 @@
                     self.wildcards.append(wcard)
 
-        except:
+        except Exception as exc:
             msg = "Loader: Error accessing Reader "
-            msg += "in %s\n  %s" % (loader.__name__, sys.exc_value)
+            msg += "in %s\n  %s" % (loader.__name__, exc)
             logger.error(msg)
         return reader_found
@@ -320,7 +320,7 @@
                         self.writers[ext].insert(0, loader.write)
 
-            except:
+            except Exception as exc:
                 msg = "Loader: Error accessing Reader"
-                msg += " in %s\n  %s" % (module.__name__, sys.exc_value)
+                msg += " in %s\n  %s" % (module.__name__, exc)
                 logger.error(msg)
         return reader_found
Index: src/sas/sascalc/dataloader/manipulations.py
===================================================================
--- src/sas/sascalc/dataloader/manipulations.py	(revision 574adc767a9e86744546f8edd6a23dc4f81914ad)
+++ src/sas/sascalc/dataloader/manipulations.py	(revision e4e9162012e4f9bbd8ce8fe25bb99135a3a37e40)
@@ -928,22 +928,23 @@
 
         # Organize the results
-        for i in range(self.nbins):
-            y[i] = y[i] / y_counts[i]
-            y_err[i] = math.sqrt(y_err[i]) / y_counts[i]
-
-            # The type of averaging: phi,q2, or q
-            # Calculate x[i]should be at the center of the bin
+        with np.errstate(divide='ignore', invalid='ignore'):
+            y = y/y_counts
+            y_err = np.sqrt(y_err)/y_counts
+            # The type of averaging: phi, q2, or q
+            # Calculate x values at the center of the bin
             if run.lower() == 'phi':
-                x[i] = (self.phi_max - self.phi_min) / self.nbins * \
-                    (1.0 * i + 0.5) + self.phi_min
+                step = (self.phi_max - self.phi_min) / self.nbins
+                x = (np.arange(self.nbins) + 0.5) * step + self.phi_min
             else:
-                # We take the center of ring area, not radius.
-                # This is more accurate than taking the radial center of ring.
-                # delta_r = (self.r_max - self.r_min) / self.nbins
-                # r_inner = self.r_min + delta_r * i
-                # r_outer = r_inner + delta_r
-                # x[i] = math.sqrt((r_inner * r_inner + r_outer * r_outer) / 2)
-                x[i] = x[i] / y_counts[i]
-        y_err[y_err == 0] = np.average(y_err)
+                # set q to the average of the q values within each bin
+                x = x/y_counts
+
+                ### Alternate algorithm
+                ## We take the center of ring area, not radius.
+                ## This is more accurate than taking the radial center of ring.
+                #step = (self.r_max - self.r_min) / self.nbins
+                #r_inner = self.r_min + step * np.arange(self.nbins)
+                #x = math.sqrt((r_inner**2 + (r_inner + step)**2) / 2)
+
         idx = (np.isfinite(y) & np.isfinite(y_err))
         if x_err is not None:
Index: src/sas/sascalc/dataloader/readers/associations.py
===================================================================
--- src/sas/sascalc/dataloader/readers/associations.py	(revision c7c814358a478ddea9a6f8b57a46374c443b4776)
+++ src/sas/sascalc/dataloader/readers/associations.py	(revision c7c814358a478ddea9a6f8b57a46374c443b4776)
@@ -54,6 +54,6 @@
                 exec("loader.associate_file_type('%s', %s)"
                      % (ext.upper(), reader))
-            except:
+            except Exception as exc:
                 msg = "read_associations: skipping association"
-                msg += " for %s\n  %s" % (ext.lower(), sys.exc_value)
+                msg += " for %s\n  %s" % (ext.lower(), exc)
                 logger.error(msg)
Index: src/sas/sascalc/dataloader/readers/cansas_reader.py
===================================================================
--- src/sas/sascalc/dataloader/readers/cansas_reader.py	(revision 2b3eb3dacf42149909e5d221a9f6ad7d6b647c31)
+++ src/sas/sascalc/dataloader/readers/cansas_reader.py	(revision 2b3eb3dacf42149909e5d221a9f6ad7d6b647c31)
@@ -1238,9 +1238,8 @@
                         conv = Converter(units)
                         setattrchain(storage, variable, conv(value, units=local_unit))
-                    except Exception:
-                        _, exc_value, _ = sys.exc_info()
+                    except Exception as exc:
                         err_mess = "CanSAS reader: could not convert"
                         err_mess += " %s unit [%s]; expecting [%s]\n  %s" \
-                            % (variable, units, local_unit, exc_value)
+                            % (variable, units, local_unit, exc)
                         self.errors.add(err_mess)
                         if optional:
Index: src/sas/sascalc/fit/BumpsFitting.py
===================================================================
--- src/sas/sascalc/fit/BumpsFitting.py	(revision 1386b2f74db29d0e69270a9a6bf68187b52db864)
+++ src/sas/sascalc/fit/BumpsFitting.py	(revision 0aeba4e4481fefcf2c9242a51fbc87e49c7b5e33)
@@ -2,4 +2,6 @@
 BumpsFitting module runs the bumps optimizer.
 """
+from __future__ import print_function
+
 import os
 from datetime import timedelta, datetime
@@ -9,6 +11,9 @@
 
 from bumps import fitters
+
 try:
     from bumps.options import FIT_CONFIG
+    # Preserve bumps default fitter in case someone wants it later
+    BUMPS_DEFAULT_FITTER = FIT_CONFIG.selected_id
     # Default bumps to use the Levenberg-Marquardt optimizer
     FIT_CONFIG.selected_id = fitters.LevenbergMarquardtFit.id
@@ -17,4 +22,6 @@
 except ImportError:
     # CRUFT: Bumps changed its handling of fit options around 0.7.5.6
+    # Preserve bumps default fitter in case someone wants it later
+    BUMPS_DEFAULT_FITTER = fitters.FIT_DEFAULT
     # Default bumps to use the Levenberg-Marquardt optimizer
     fitters.FIT_DEFAULT = 'lm'
@@ -126,4 +133,5 @@
         if initial_values is not None:
             self._reset_pars(fitted, initial_values)
+        #print("constraints", constraints)
         self.constraints = dict(constraints)
         self.set_fitted(fitted)
@@ -222,10 +230,11 @@
     def _setup(self):
         exprs = {}
-        for M in self.models:
-            exprs.update((".".join((M.name, k)), v) for k, v in M.constraints.items())
+        for model in self.models:
+            exprs.update((".".join((model.name, k)), v)
+                         for k, v in model.constraints.items())
         if exprs:
-            symtab = dict((".".join((M.name, k)), p)
-                          for M in self.models
-                          for k, p in M.parameters().items())
+            symtab = dict((".".join((model.name, k)), p)
+                          for model in self.models
+                          for k, p in model.parameters().items())
             self.update = compile_constraints(symtab, exprs)
         else:
Index: src/sas/sascalc/fit/expression.py
===================================================================
--- src/sas/sascalc/fit/expression.py	(revision 574adc767a9e86744546f8edd6a23dc4f81914ad)
+++ src/sas/sascalc/fit/expression.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -210,5 +210,5 @@
 
     #print("Function: "+functiondef)
-    exec functiondef in globals,locals
+    exec(functiondef, globals, locals)
     retfn = locals['eval_expressions']
 
Index: src/sas/sascalc/fit/fitstate.py
===================================================================
--- src/sas/sascalc/fit/fitstate.py	(revision f6f3fb410833ff629c71f334bd88fb3bbf077058)
+++ src/sas/sascalc/fit/fitstate.py	(revision f6f3fb410833ff629c71f334bd88fb3bbf077058)
@@ -0,0 +1,407 @@
+"""
+Interface to page state for saved fits.
+
+The 4.x sasview gui builds the model, smearer, etc. directly inside the wx
+GUI code.  This code separates the in-memory representation from the GUI.
+Initially it is used for a headless bumps fitting backend that operates
+directly on the saved XML; eventually it should provide methods to replace
+direct access to the PageState object so that the code for setting up and
+running fits in wx, qt, and headless versions of SasView shares a common
+in-memory representation.
+"""
+from __future__ import print_function, division
+
+import copy
+from collections import namedtuple
+
+import numpy as np
+
+from bumps.names import FitProblem
+
+from sasmodels.core import load_model_info
+from sasmodels.data import plot_theory
+from sasmodels.sasview_model import _make_standard_model, MultiplicationModel, load_custom_model
+from sasmodels.weights import MODELS as POLYDISPERSITY_MODELS
+
+from .pagestate import Reader, PageState, SimFitPageState, CUSTOM_MODEL
+from .BumpsFitting import SasFitness, ParameterExpressions
+from .AbstractFitEngine import FitData1D, FitData2D, Model
+from .models import PLUGIN_NAME_BASE, find_plugins_dir
+from .qsmearing import smear_selection
+
+# Monkey patch SasFitness class with plotter
+def sasfitness_plot(self, view='log'):
+    data, theory, resid = self.data.sas_data, self.theory(), self.residuals()
+    plot_theory(data, theory, resid, view)
+SasFitness.plot = sasfitness_plot
+
+# Use a named tuple for the sasview parameters
+PARAMETER_FIELDS = [
+    "fitted", "name", "value", "plusminus", "uncertainty",
+    "lower", "upper", "units",
+    ]
+SasviewParameter = namedtuple("Parameter", PARAMETER_FIELDS)
+
+class FitState(object):
+    def __init__(self, fitfile):
+        self.fitfile = fitfile
+        self.simfit = None
+        self.fits = []
+
+        reader = Reader(self._add_entry)
+        datasets = reader.read(fitfile)
+        self._set_constraints()
+        #print("loaded", datasets)
+
+    def _add_entry(self, state=None, datainfo=None, format=None):
+        """
+        Handed to the reader to receive and accumulate the loaded state objects.
+        """
+        # Note: datainfo is in state.data; format=.svs means reset fit panels
+        if isinstance(state, PageState):
+            # TODO: shouldn't the update be part of the load?
+            state._convert_to_sasmodels()
+            self.fits.append(state)
+        elif isinstance(state, SimFitPageState):
+            self.simfit = state
+        else:
+            # ignore empty fit info
+            pass
+
+    def __str__(self):
+        return '<SasFit %s>'%self.fitfile
+
+    def show(self):
+        """
+        Summarize the fit pages in the state object.
+        """
+        for k, fit in enumerate(self.fits):
+            print("="*20, "Fit page", k+1)
+            #print(fit)
+            for attr, value in sorted(fit.__dict__.items()):
+                if isinstance(value, (list, tuple)):
+                    print(attr)
+                    for item in value:
+                        print("   ", item)
+                else:
+                    print(attr, value)
+        if self.simfit:
+            print("="*20, "Constraints")
+            #print(self.simfit)
+            for attr, value in sorted(self.simfit.__dict__.items()):
+                if isinstance(value, (list, tuple)):
+                    print(attr)
+                    for item in value:
+                        print("   ", item)
+                else:
+                    print(attr, value)
+
+    def make_fitproblem(self):
+        """
+        Build collection of bumps fitness calculators and return the FitProblem.
+        """
+        # TODO: batch info not stored with project/analysis file (ticket #907)
+        models = [make_fitness(state) for state in self.fits]
+        if not models:
+            raise RuntimeError("Nothing to fit")
+        fit_problem = FitProblem(models)
+        fit_problem.setp_hook = ParameterExpressions(models)
+        return fit_problem
+
+    def _set_constraints(self):
+        """
+        Adds fit_page and constraints list to each model.
+        """
+        # early return if no sim fit
+        if self.simfit is None:
+            for fit in self.fits:
+                fit.fit_page = 'M1'
+                fit.constraints = {}
+            return
+
+        # Note: simfitpage.py:load_from_save_state relabels the model and
+        # constraint on load, replacing the model name in the constraint
+        # expression with the new name for every constraint expression. We
+        # don't bother to do that here since we don't need to relabel the
+        # model ids.
+        constraints = {}
+        for item in self.simfit.constraints_list:
+            # model_cbox in the constraints list should match fit_page_source
+            # in the model list.
+            pairs = constraints.setdefault(item['model_cbox'], [])
+            pairs.append((item['param_cbox'], item['constraint']))
+
+        # No way to uniquely map the page id (M1, M2, etc.) to the different
+        # items in fits; neither fit_number nor fit_page_source is in the
+        # pagestate for the fit, and neither model_name nor data name are
+        # unique.  The usual case of one model per data file will get us most
+        # of the way there but there will be ambiguity when the data file
+        # is not unique, e.g., when different parts of the data set are
+        # fit with different models.  If the same model and same data are
+        # used (e.g., with different background, scale or resolution in
+        # different segments) then the model-fit association will be assigned
+        # arbitrarily based on whichever happens to come first.
+        used = []
+        for model in self.simfit.model_list:
+            for fit in self.fits:
+                #print(model['name'], fit.data_id, model_name(fit), model['model_name'])
+                if (fit.data_id == model['name']
+                        and model_name(fit) == model['model_name']
+                        and fit not in used):
+                    fit.fit_page = model['fit_page_source']
+                    fit.constraints = constraints.setdefault(fit.fit_page, [])
+                    used.append(fit)
+                    break
+            else:
+                raise ValueError("could not find model %s in file"
+                                 % model['fit_page_source'])
+
+
+def model_name(state):
+    """
+    Build the model name out of form factor and structure factor (if present).
+
+    This will be the name that is stored as the model name in the simultaneous
+    fit model_list structure corresponding to the form factor and structure
+    factor given on the individual fit pages.  The model name is used to help
+    disambiguate different SASentry sections with the same dataset.
+    """
+    p_model, s_model = state.formfactorcombobox, state.structurecombobox
+    if s_model is not None and s_model != "" and s_model.lower() != "none":
+        return '*'.join((p_model, s_model))
+    else:
+        return p_model
+
+def get_data_weight(state):
+    """
+    Get error bars on data.  These could be the values computed by reduction
+    and stored in the file, the square root of the intensity (if instensity
+    is approximately counts), the intensity itself (would be better as a
+    percentage of the intensity, such as 2% or 5% depending on relative
+    counting time), or one for equal weight uncertainty depending on the
+    value of state.dI_*.
+    """
+    # Cribbed from perspectives/fitting/utils.py:get_weight and
+    # perspectives/fitting/fitpage.py: get_weight_flag
+    weight = None
+    if state.enable2D:
+        dy_data = state.data.err_data
+        data = state.data.data
+    else:
+        dy_data = state.data.dy
+        data = state.data.y
+    if state.dI_noweight:
+        weight = np.ones_like(data)
+    elif state.dI_didata:
+        weight = dy_data
+    elif state.dI_sqridata:
+        weight = np.sqrt(np.abs(data))
+    elif state.dI_idata:
+        weight = np.abs(data)
+    return weight
+
+_MODEL_CACHE = {}
+def load_model(name):
+    """
+    Given a model name load the Sasview shim model from sasmodels.
+
+    If name starts with "[Plug-in]" then load it as a custom model from the
+    plugins directory.  This code does not go through the Sasview model manager
+    interface since that loads all available models rather than just those
+    needed.
+    """
+    # Remember the models that are loaded so they are only loaded once.  While
+    # not strictly necessary (the models will use identical but different model
+    # info structure) it saves a little time and memory for the usual case
+    # where models are reused for simultaneous and batch fitting.
+    if name in _MODEL_CACHE:
+        return _MODEL_CACHE[name]
+    if name.startswith(PLUGIN_NAME_BASE):
+        name = name[len(PLUGIN_NAME_BASE):]
+        plugins_dir = find_plugins_dir()
+        path = os.path.abspath(os.path.join(plugins_dir, name + ".py"))
+        #print("loading custom", path)
+        model = load_custom_model(path)
+    elif name and name is not None and name.lower() != "none":
+        #print("loading standard", name)
+        model = _make_standard_model(name)
+    else:
+        model = None
+    _MODEL_CACHE[name] = model
+    return model
+
+def parse_optional_float(value):
+    """
+    Convert optional floating point from string to value, returning None
+    if string is None, empty or contains the word "None" (case insensitive).
+    """
+    if value is not None and value != "" and value.lower() != "none":
+        return float(value)
+    else:
+        return None
+
+def make_fitness(state):
+    # Load the model
+    category_name = state.categorycombobox
+    form_factor_name = state.formfactorcombobox
+    structure_factor_name = state.structurecombobox
+    multiplicity = state.multi_factor
+    if category_name == CUSTOM_MODEL:
+        assert form_factor_name.startswith(PLUGIN_NAME_BASE)
+    form_factor_model = load_model(form_factor_name)
+    structure_factor_model = load_model(structure_factor_name)
+    model = form_factor_model(multiplicity)
+    if structure_factor_model is not None:
+        model = MultiplicationModel(model, structure_factor_model())
+
+    # Set the dispersity distributions for all model parameters.
+    # Default to gaussian
+    dists = {par_name + ".type": "gaussian" for par_name in model.dispersion}
+    dists.update(state.disp_obj_dict)
+    for par_name, dist_name in state.disp_obj_dict.items():
+        dispersion = POLYDISPERSITY_MODELS[dist_name]()
+        if dist_name == "array":
+            dispersion.set_weights(state.values[par_name], state.weights[par_name])
+        base_par = par_name.replace('.width', '')
+        model.set_dispersion(base_par, dispersion)
+
+    # Put parameter values and ranges into the model
+    fitted = []
+    for par_tuple in state.parameters + state.fixed_param + state.fittable_param:
+        par = SasviewParameter(*par_tuple)
+        if par.name not in state.weights:
+            # Don't try to set parameter values for array distributions
+            # TODO: keep weights filename in the array distribution object
+            model.setParam(par.name, parse_optional_float(par.value))
+        if par.fitted:
+            fitted.append(par.name)
+        if par.name in model.details:
+            lower = parse_optional_float(par.lower[1])
+            upper = parse_optional_float(par.upper[1])
+            model.details[par.name] = [par.units, lower, upper]
+    #print("pars", model.params)
+    #print("limits", model.details)
+    #print("fitted", fitted)
+
+    # Set the resolution
+    data = copy.deepcopy(state.data)
+    if state.disable_smearer:
+        smearer = None
+    elif state.enable_smearer:
+        smearer = smear_selection(data, model)
+    elif state.pinhole_smearer:
+        # see sasgui/perspectives/fitting/basepage.py: reset_page_helper
+        dx_percent = state.dx_percent
+        if state.dx_old:
+            dx_percent = 100*(state.dx_percent / data.x[0])
+        # see sasgui/perspectives/fitting/fitpage.py: _set_pinhole_smear
+        percent = dx_percent / 100.
+        if state.enable2D:
+            # smear_type is Pinhole2D.
+            # dqx_data, dqy_data initialized to 0
+            data.dqx_data = percent * data.qx_data
+            data.dqy_data = percent * data.qy_data
+        else:
+            data.dx = percent * data.x
+            data.dxl = data.dxw = None  # be sure it is not slit-smeared
+        smearer = smear_selection(data, model)
+    elif state.slit_smearer:
+        # see sasgui/perspectives/fitting/fitpage.py: _set_pinhole_smear
+        data_len = len(data.x)
+        data.dx = None
+        data.dxl = (state.dxl if state.dxl is not None else 0.) * np.ones(data_len)
+        data.dxw = (state.dxw if state.dxw is not None else 0.) * np.ones(data_len)
+        smearer = smear_selection(data, model)
+    else:
+        raise ValueError("expected resolution specification for fit")
+
+    # Set the data weighting (dI, sqrt(I), I, or uniform)
+    weight = get_data_weight(state)
+
+    # Make fit data object and set the data weights
+    # TODO: check 2D masked data
+    if state.enable2D:
+        fitdata = FitData2D(sas_data2d=data, data=data.data,
+                            err_data=data.err_data)
+        fitdata.err_data = weight
+    else:
+        data.mask = (np.isnan(data.y) if data.y is not None
+                        else np.zeros_like(data.x, dtype='bool'))
+        fitdata = FitData1D(x=data.x, y=data.y,
+                            dx=data.dx, dy=data.dy, smearer=smearer)
+        fitdata.dy = weight
+        fitdata.set_fit_range(qmin=state.qmin, qmax=state.qmax)
+    fitdata.sas_data = data
+
+    # Don't need initial values since they have been stuffed into the model
+    # If provided, then they should be one-to-one with the parameter names
+    # listed in fitted.
+    initial_values = None
+
+    fitmodel = Model(model, fitdata)
+    fitmodel.name = state.fit_page
+    fitness = SasFitness(
+        model=fitmodel,
+        data=fitdata,
+        constraints=state.constraints,
+        fitted=fitted,
+        initial_values=initial_values,
+        )
+
+    return fitness
+
+class BumpsPlugin:
+    """
+    Object holding methods for interacting with SasView using the direct
+    bumps interface.
+    """
+    #@staticmethod
+    #def data_view():
+    #    pass
+
+    #@staticmethod
+    #def model_view():
+    #    pass
+
+    @staticmethod
+    def load_model(filename):
+        fit = FitState(filename)
+        #fit.show()
+        #print("====\nfit", fit)
+        problem = fit.make_fitproblem()
+        #print(problem.show())
+        return problem
+
+    #@staticmethod
+    #def new_model():
+    #    pass
+
+
+def setup_sasview():
+    from sas.sasview.sasview import setup_logging, setup_mpl, setup_sasmodels
+    #setup_logging()
+    #setup_mpl()
+    setup_sasmodels()
+
+def setup_bumps():
+    """
+    Install the refl1d plugin into bumps, but don't run main.
+    """
+    import os
+    import bumps.cli
+    bumps.cli.set_mplconfig(appdatadir=os.path.join('.sasview', 'bumpsfit'))
+    bumps.cli.install_plugin(BumpsPlugin)
+
+def bumps_cli():
+    """
+    Install the Refl1D plugin into bumps and run the command line interface.
+    """
+    setup_sasview()
+    setup_bumps()
+    import bumps.cli
+    bumps.cli.main()
+
+if __name__ == "__main__":
+    # Allow run with:
+    #    python -m sas.sascalc.fit.fitstate
+    bumps_cli()
Index: src/sas/sascalc/fit/models.py
===================================================================
--- src/sas/sascalc/fit/models.py	(revision 0f0fe42a04c7184507c90d05228beb99cab4a718)
+++ src/sas/sascalc/fit/models.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -12,4 +12,6 @@
 import py_compile
 import shutil
+
+from six import reraise
 
 from sasmodels.sasview_model import load_custom_model, load_standard_models
@@ -62,8 +64,7 @@
     try:
         new_instance = model()
-    except Exception:
-        msg = "Plugin %s error in __init__ \n\t: %s %s\n" % (str(name),
-                                                             str(sys.exc_type),
-                                                             sys.exc_info()[1])
+    except Exception as exc:
+        msg = ("Plugin %s error in __init__ \n\t: %s %s\n"
+               % (name, type(exc), exc))
         plugin_log(msg)
         return None
@@ -72,7 +73,7 @@
         try:
             value = new_instance.function()
-        except Exception:
+        except Exception as exc:
             msg = "Plugin %s: error writing function \n\t :%s %s\n " % \
-                    (str(name), str(sys.exc_type), sys.exc_info()[1])
+                    (str(name), str(type(exc)), exc)
             plugin_log(msg)
             return None
@@ -139,5 +140,5 @@
         if type is not None and issubclass(type, py_compile.PyCompileError):
             print("Problem with", repr(value))
-            raise type, value, tb
+            reraise(type, value, tb)
         return 1
 
@@ -153,6 +154,6 @@
         compileall.compile_dir(dir=dir, ddir=dir, force=0,
                                quiet=report_problem)
-    except Exception:
-        return sys.exc_info()[1]
+    except Exception as exc:
+        return exc
     return None
 
@@ -185,5 +186,5 @@
                     model.name = PLUGIN_NAME_BASE + model.name
                 plugins[model.name] = model
-            except Exception:
+            except Exception as exc:
                 msg = traceback.format_exc()
                 msg += "\nwhile accessing model in %r" % path
Index: src/sas/sascalc/fit/pagestate.py
===================================================================
--- src/sas/sascalc/fit/pagestate.py	(revision 863ac2c13917d1e25dc3fe0061ef11ef07b02366)
+++ src/sas/sascalc/fit/pagestate.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -1,4 +1,135 @@
 """
 Class that holds a fit page state
+
+Pagestate fields reflect the names of the gui controls from the sasview 3.x
+fit page, so they are somewhat difficult to interpret.
+
+Pagestate attributes are as follows:
+
+    # =>name: desc       indicates the attribute is derived
+    # name(xml): desc    indicates the attribute name differs from the xml tag
+
+    # SasView version which saved the file
+    version: (4, 1, 2) from <fitting_plug_in version="major.minor.point">
+
+    # Session information
+    group_id: unique id for fit page in running system (int)
+    => data_group_id: unique id for data item in running system (None)
+
+    # Data file
+    data: contents of <SASdata> (Data1D or Data2D)
+    data_name: filename + [num] (latex_smeared.xml [1])
+    data_id: filename + [num] + timestamp (latex_smeared.xml [1]1523303027.73)
+    file: filename + [date time] (latex_smeared.xml [Apr 09 15:45])
+    name: ?? (None)
+    npts: number of points (float)
+    enable2D: True if data is 2D (or if model is 2D and no data)
+    is_data: True (pagestate will not save if there is no data attached)
+
+    # Data weighting
+    dI_didata: True if dy = data.dy
+    dI_idata: True if dy = data.y
+    dI_noweight: True if dy = 1
+    dI_sqrdata: True if dy = sqrt(data.y)
+
+    # Data selection
+    qmax: maximum q (float)
+    qmin: minimum q (float)
+    => qmax_x: ?? (None)
+    => qmin_x: ?? (None)
+
+    # Resolution smearing
+    enable_smearer: True if use data.dx
+    disable_smearer: True if no smearing
+    pinhole_smearer: True if custom pinhole smear
+    slit_smearer: True if custom slit smear
+    dq_l: 2D resolution <dQp>
+    dq_r: 2D resolution <dQs>
+    dx_old: True for 3.x version of custom pinhole, which used dx_min rather
+        than dx_percent, with dx_percent interpreted as 100 * dx_percent/q[0]
+    dx_percent: custom pinhole resolution percentage
+    dxl: slit height for custom slit resolution
+    dxw: slit width for custom slit resolution
+    smearer: repr() for active smearer (None on load)
+    smear_type: None (None on load)
+
+    # Model selection
+    categorycombobox: model category
+    formfactorcombobox: model name (could be "[plug-in] name")
+    structurecombobox: structure factor model name (string or None or "None")
+    multi_factor: multiplicity (integer or None)
+    magnetic_on: True if model is magnetic (only for 2D data for now)
+    => model: active model object (None on load)
+
+    # Model parameters
+    # Parameter is a tuple with the following structure.  The parentheses
+    # indicate xml attribute for the <parameter .../> tag:
+    #    fitted(selected_to_fit): True if parameter is fitted
+    #    name(name): display name for the parameter (string)
+    #    value(value): displayed parameter value (string)
+    #    => plusminus: '+/-' (constant string)
+    #    => uncertainty: tuple
+    #        (uncertainty_displayed): True if there is an uncertainty
+    #        (uncertainty_value): displayed uncertainty (string)
+    #    => lower: tuple
+    #        (minimum_displayed): True if there is a lower bound
+    #        (minimum_value): displayed lower bound (string)
+    #    => upper: tuple
+    #        (maximum_displayed): True if there is a upper bound
+    #        (maximum_value): displayed upper bound (string)
+    #    units(unit): displayed units
+    parameters: list of normal parameters
+    fixed_param: list of non-fitting parameters (nsigma, npts in dispersity)
+    fittable_param: list of fittable dispersity parameters (distribution width)
+    str_parameters: list of selection parameters (e.g, shell forms in spherical_sld)
+    orientation_params(orientation_parameters): list of orientation and
+        magnetic parameters (already included in parameters, so safe to ignore)
+    orientation_params_disp(dispersity_parameters): list of orientation
+        disperisty parameters (already included in fixed_param and
+        fittable_param so safe to ignore)
+
+    # Dispersity controls
+    enable_disp: True if dispersity parameters
+    disable_disp: True if no dispersity parameters
+    disp_obj_dict(disp_obj): {'parameter.width': 'dispersity distribution'}
+    values: {'parameter.width': [array distribution parameter values] }
+    weights: {'parameter.width': [array distribution parameter weights] }
+    => disp_box 0
+    => disp_cb_dict {}
+    => disp_list []
+
+    # Simultaneous fitting
+
+    => images: None (initialized but unused?)
+    => reset: False (initialized but unused?)
+    => event_owner None
+    => m_name None
+    => manager None
+    => page_name
+    => param_toFit: []
+    => process: list of process done on object [] (maybe managed by guiframe?)
+    => saved_states {}
+    => cb1: False (simfit cb1 is now stored in select_all)
+
+    tcChi 1.3463
+    theory_data None
+    timestamp 1523303103.74
+
+Constraint attributes are as follows:
+
+    constraint_dict {}
+    constraints_list
+        {'model_cbox': 'M2', 'param_cbox': 'scale', 'egal_txt': ' = ', 'constraint': 'M1.scale'}
+        {'model_cbox': 'M2', 'param_cbox': 'radius', 'egal_txt': ' = ', 'constraint': 'M1.radius'}
+        {'model_cbox': 'M2', 'param_cbox': 'radius.width', 'egal_txt': ' = ', 'constraint': 'M1.radius.width'}
+    fit_page_no None
+    model_list
+        {'fit_number': '393', 'checked': 'True', 'fit_page_source': 'M2', 'name': 'latex_smeared.xml   [1]1523535051.03', 'model_name': 'sphere'}
+        {'fit_number': '335', 'checked': 'True', 'fit_page_source': 'M1', 'name': 'latex_smeared.xml  1523535050.03', 'model_name': 'sphere'}
+    model_to_fit
+    no_constraint 0
+    select_all True
+
+
 """
 # TODO: Refactor code so we don't need to use getattr/setattr
@@ -162,5 +293,4 @@
         self.theory_data = None
         # Is 2D
-        self.is_2D = False
         self.images = None
 
@@ -557,10 +687,7 @@
         temp_parameters = []
         temp_fittable_param = []
-        if self.data.__class__.__name__ == "Data2D":
-            self.is_2D = True
-        else:
-            self.is_2D = False
         if self.data is not None:
-            if not self.is_2D:
+            is_2D = (self.data.__class__.__name__ == "Data2D")
+            if not is_2D:
                 for item in self.parameters:
                     if item not in self.orientation_params:
@@ -650,7 +777,7 @@
                     #Truncating string so print doesn't complain of being outside margins
                     if sys.platform != "win32":
-                        MAX_STRING_LENGHT = 50
-                        if len(file_value) > MAX_STRING_LENGHT:
-                            file_value = "File name:.."+file_value[-MAX_STRING_LENGHT+10:]
+                        MAX_STRING_LENGTH = 50
+                        if len(file_value) > MAX_STRING_LENGTH:
+                            file_value = "File name:.."+file_value[-MAX_STRING_LENGTH+10:]
                     file_name = CENTRE % file_value
                     if len(title) == 0:
@@ -905,5 +1032,5 @@
                 doc_model = newdoc.createElement('model_list_item')
                 doc_model.setAttribute('checked', str(model[0].GetValue()))
-                keys = model[1].keys()
+                keys = list(model[1].keys())
                 doc_model.setAttribute('name', str(keys[0]))
                 values = model[1].get(keys[0])
@@ -964,5 +1091,5 @@
         if node.get('version'):
             # Get the version for model conversion purposes
-            x = re.sub('[^\d.]', '', node.get('version'))
+            x = re.sub(r'[^\d.]', '', node.get('version'))
             self.version = tuple(int(e) for e in str.split(x, "."))
             # The tuple must be at least 3 items long
@@ -984,7 +1111,7 @@
                 try:
                     self.timestamp = float(entry.get('epoch'))
-                except Exception:
+                except Exception as exc:
                     msg = "PageState.fromXML: Could not"
-                    msg += " read timestamp\n %s" % sys.exc_value
+                    msg += " read timestamp\n %s" % exc
                     logger.error(msg)
 
@@ -1282,5 +1409,5 @@
                         if isinstance(data.run_name, dict):
                             # Note: key order in dict is not guaranteed, so sort
-                            name = data.run_name.keys()[0]
+                            name = list(data.run_name.keys())[0]
                         else:
                             name = data.run_name
Index: src/sas/sascalc/invariant/invariant.py
===================================================================
--- src/sas/sascalc/invariant/invariant.py	(revision 574adc767a9e86744546f8edd6a23dc4f81914ad)
+++ src/sas/sascalc/invariant/invariant.py	(revision dbfd307df25dbe7befe02b50ded9d06ccf6499e3)
@@ -344,5 +344,8 @@
         else:
             A = np.vstack([linearized_data.x / linearized_data.dy, 1.0 / linearized_data.dy]).T
-            (p, residuals, _, _) = np.linalg.lstsq(A, linearized_data.y / linearized_data.dy)
+            # CRUFT: numpy>=1.14.0 allows rcond=None for the following default
+            rcond = np.finfo(float).eps * max(A.shape)
+            p, residuals, _, _ = np.linalg.lstsq(A, linearized_data.y / linearized_data.dy,
+                                                 rcond=rcond)
 
             # Get the covariance matrix, defined as inv_cov = a_transposed * a
Index: src/sas/sascalc/pr/distance_explorer.py
===================================================================
--- src/sas/sascalc/pr/distance_explorer.py	(revision 959eb0181ad06828a748f04512f953aad5b31659)
+++ src/sas/sascalc/pr/distance_explorer.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -99,8 +99,8 @@
                 results.pos_err.append(pos_err)
                 results.osc.append(osc)
-            except:
+            except Exception as exc:
                 # This inversion failed, skip this D_max value
                 msg = "ExploreDialog: inversion failed for "
-                msg += "D_max=%s\n %s" % (str(d), sys.exc_value)
+                msg += "D_max=%s\n %s" % (str(d), exc)
                 results.errors.append(msg)
 
Index: src/sas/sascalc/pr/fit/expression.py
===================================================================
--- src/sas/sascalc/pr/fit/expression.py	(revision 574adc767a9e86744546f8edd6a23dc4f81914ad)
+++ src/sas/sascalc/pr/fit/expression.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -210,5 +210,5 @@
 
     #print("Function: "+functiondef)
-    exec functiondef in globals,locals
+    exec(functiondef, globals, locals)
     retfn = locals['eval_expressions']
 
Index: src/sas/sascalc/pr/invertor.py
===================================================================
--- src/sas/sascalc/pr/invertor.py	(revision 57e48ca3fba05a5ace7a3f2a5b6ff8f1a9f4206c)
+++ src/sas/sascalc/pr/invertor.py	(revision dbfd307df25dbe7befe02b50ded9d06ccf6499e3)
@@ -474,5 +474,7 @@
 
         # Perform the inversion (least square fit)
-        c, chi2, _, _ = lstsq(a, b, rcond=-1)
+        # CRUFT: numpy>=1.14.0 allows rcond=None for the following default
+        rcond = np.finfo(float).eps * max(a.shape)
+        c, chi2, _, _ = lstsq(a, b, rcond=rcond)
         # Sanity check
         try:
Index: src/sas/sascalc/pr/num_term.py
===================================================================
--- src/sas/sascalc/pr/num_term.py	(revision 3e6829d03839dc7a60382a887dc7f27da1c50c8f)
+++ src/sas/sascalc/pr/num_term.py	(revision 3e6829d03839dc7a60382a887dc7f27da1c50c8f)
@@ -182,6 +182,6 @@
                 data_y = np.append(data_y, test_y)
                 data_err = np.append(data_err, err)
-            except:
-                logger.error(sys.exc_value)
+            except Exception as exc:
+                logger.error(exc)
 
     return data_x, data_y, data_err
Index: src/sas/sascalc/realspace/VolumeCanvas.py
===================================================================
--- src/sas/sascalc/realspace/VolumeCanvas.py	(revision 98e3f24eb417c960360e014aae48605345a05266)
+++ src/sas/sascalc/realspace/VolumeCanvas.py	(revision e090ba909aeb28ce2f0504a77e5a800070907873)
@@ -472,5 +472,5 @@
             Return a list of the shapes
         """
-        return self.shapes.keys()
+        return list(self.shapes.keys())
 
     def _addSingleShape(self, shapeDesc):
Index: src/sas/sasgui/perspectives/corfunc/media/corfunc_help.rst
===================================================================
--- src/sas/sasgui/perspectives/corfunc/media/corfunc_help.rst	(revision 490f79075e15c069f2a131d3ccd6a1e510e7dcfe)
+++ src/sas/sasgui/perspectives/corfunc/media/corfunc_help.rst	(revision 4d0666809b8cd216e5fe489e7e0b84ee88c2e188)
@@ -30,4 +30,9 @@
 of the profile provides measures of the layer thickness, and the area under 
 the profile is related to the amount of material that is adsorbed.
+
+.. note::
+    These transforms assume that the data has been measured on a pinhole-
+    collimated instrument or, if not, that the data has been Lorentz-
+    corrected beforehand.
 
 Both analyses are performed in 3 stages:
Index: src/sas/sasgui/perspectives/fitting/fitpage.py
===================================================================
--- src/sas/sasgui/perspectives/fitting/fitpage.py	(revision ca4f40d38aa2c0abd8d7a1571cf953a85a676091)
+++ src/sas/sasgui/perspectives/fitting/fitpage.py	(revision ca4f40d38aa2c0abd8d7a1571cf953a85a676091)
@@ -56,5 +56,4 @@
         # draw sizer
         self._fill_data_sizer()
-        self.is_2D = None
         self.fit_started = False
         self.weightbt_string = None
Index: src/sas/sasgui/plottools/PlotPanel.py
===================================================================
--- src/sas/sasgui/plottools/PlotPanel.py	(revision 2469df7fc9ffa047115c5bffe589ed307b12c363)
+++ src/sas/sasgui/plottools/PlotPanel.py	(revision 75313af5ff7a47b13075dd439585c592071aebaf)
@@ -1434,4 +1434,5 @@
 
         """
+        # TODO: include mask info in plotter
         self.data = data
         self.qx_data = qx_data
@@ -1451,35 +1452,24 @@
         else:
             output = copy.deepcopy(self.data)
-        # check scale
+        # rescale data if necessary
         if self.scale == 'log_{10}':
-            try:
-                if  self.zmin_2D <= 0  and len(output[output > 0]) > 0:
-                    zmin_temp = self.zmin_2D
-                    output[output > 0] = np.log10(output[output > 0])
-                    #In log scale Negative values are not correct in general
-                    #output[output<=0] = math.log(np.min(output[output>0]))
-                elif self.zmin_2D <= 0:
-                    zmin_temp = self.zmin_2D
-                    output[output > 0] = np.zeros(len(output))
-                    output[output <= 0] = -32
-                else:
-                    zmin_temp = self.zmin_2D
-                    output[output > 0] = np.log10(output[output > 0])
-                    #In log scale Negative values are not correct in general
-                    #output[output<=0] = math.log(np.min(output[output>0]))
-            except:
-                #Too many problems in 2D plot with scale
-                pass
-
-        else:
-            zmin_temp = self.zmin_2D
+            with np.errstate(all='ignore'):
+                output = np.log10(output)
+            index = np.isfinite(output)
+            if not index.all():
+                cutoff = (np.min(output[index]) - np.log10(2)
+                          if index.any() else 0.)
+                output[~index] = cutoff
+        # TODO: fix handling of zmin_2D/zmax_2D in _onToggleScale
+        # For now, use default vmin/vmax from data
+        #vmin, vmax = self.zmin_2D, self.zmax_2D
+        vmin, vmax = None, None
         self.cmap = cmap
         if self.dimension != 3:
             #Re-adjust colorbar
             self.subplot.figure.subplots_adjust(left=0.2, right=.8, bottom=.2)
-
             im = self.subplot.imshow(output, interpolation='nearest',
                                      origin='lower',
-                                     vmin=zmin_temp, vmax=self.zmax_2D,
+                                     vmin=vmin, vmax=vmax,
                                      cmap=self.cmap,
                                      extent=(self.xmin_2D, self.xmax_2D,
Index: test/calculatorview/test/utest_gui_sld.py
===================================================================
--- test/calculatorview/test/utest_gui_sld.py	(revision 959eb0181ad06828a748f04512f953aad5b31659)
+++ test/calculatorview/test/utest_gui_sld.py	(revision 88d2e70bb28d8f9c855c86d94ad8ddd63154277b)
@@ -33,5 +33,5 @@
         self.sld_frame.panel.ProcessEvent(clickEvent)
         bkg = self.sld_frame.panel.compound_ctl.GetBackgroundColour()
-        self.assert_(bkg.GetAsString() == 'pink')
+        self.assertTrue(bkg.GetAsString() == 'pink')
         #compute invariant without entering a value for compound
         self.sld_frame.panel.compound_ctl.SetValue("")
@@ -39,5 +39,5 @@
         self.sld_frame.panel.ProcessEvent(clickEvent)
         bkg = self.sld_frame.panel.compound_ctl.GetBackgroundColour()
-        self.assert_(bkg.GetAsString() == 'pink')
+        self.assertTrue(bkg.GetAsString() == 'pink')
         #compute invariant without entering a value for compound
         self.sld_frame.panel.compound_ctl.SetValue("H2O")
@@ -45,5 +45,5 @@
         self.sld_frame.panel.ProcessEvent(clickEvent)
         bkg = self.sld_frame.panel.compound_ctl.GetBackgroundColour()
-        self.assert_(bkg.GetAsString() == 'white')
+        self.assertTrue(bkg.GetAsString() == 'white')
        
     def testDensityTextCtrl(self):
@@ -59,5 +59,5 @@
         self.sld_frame.panel.ProcessEvent(clickEvent)
         bkg = self.sld_frame.panel.density_ctl.GetBackgroundColour()
-        self.assert_(bkg.GetAsString() == 'pink')
+        self.assertTrue(bkg.GetAsString() == 'pink')
         #compute invariant without entering a value for density
         self.sld_frame.panel.compound_ctl.SetValue("H2O")
@@ -65,5 +65,5 @@
         self.sld_frame.panel.ProcessEvent(clickEvent)
         bkg = self.sld_frame.panel.density_ctl.GetBackgroundColour()
-        self.assert_(bkg.GetAsString() == 'pink')
+        self.assertTrue(bkg.GetAsString() == 'pink')
         #compute invariant without entering a value for density
         self.sld_frame.panel.compound_ctl.SetValue("H2O")
@@ -71,5 +71,5 @@
         self.sld_frame.panel.ProcessEvent(clickEvent)
         bkg = self.sld_frame.panel.density_ctl.GetBackgroundColour()
-        self.assert_(bkg.GetAsString() == 'white')
+        self.assertTrue(bkg.GetAsString() == 'white')
         
     def testWavelengthTextCtrl(self):
@@ -86,5 +86,5 @@
         self.sld_frame.panel.ProcessEvent(clickEvent)
         bkg = self.sld_frame.panel.wavelength_ctl.GetBackgroundColour()
-        self.assert_(bkg.GetAsString() == 'pink')
+        self.assertTrue(bkg.GetAsString() == 'pink')
         #compute invariant without entering a value for wavelegnth
         self.sld_frame.panel.compound_ctl.SetValue("H2O")
@@ -93,11 +93,11 @@
         self.sld_frame.panel.ProcessEvent(clickEvent)
         cp_bkg = self.sld_frame.panel.compound_ctl.GetBackgroundColour()
-        self.assert_(cp_bkg.GetAsString() == 'white')
+        self.assertTrue(cp_bkg.GetAsString() == 'white')
         ds_bkg = self.sld_frame.panel.density_ctl.GetBackgroundColour()
-        self.assert_(ds_bkg.GetAsString() == 'white')
+        self.assertTrue(ds_bkg.GetAsString() == 'white')
         wv_bkg = self.sld_frame.panel.wavelength_ctl.GetBackgroundColour()
         value = self.sld_frame.panel.wavelength_ctl.GetValue()
-        self.assert_(wv_bkg.GetAsString() == 'white')
-        self.assert_(float(value) == WAVELENGTH)
+        self.assertTrue(wv_bkg.GetAsString() == 'white')
+        self.assertTrue(float(value) == WAVELENGTH)
         sld_real = self.sld_frame.panel.neutron_sld_real_ctl.GetValue()
         sld_im = self.sld_frame.panel.neutron_sld_im_ctl.GetValue()
@@ -110,17 +110,17 @@
         length = self.sld_frame.panel.neutron_length_ctl.GetValue()
         
-        self.assertAlmostEquals(float(sld_real), 1.04e-6, 1)
-        self.assertAlmostEquals(float(sld_im), -1.5e-7, 1)
+        self.assertAlmostEqual(float(sld_real), 1.04e-6, 1)
+        self.assertAlmostEqual(float(sld_im), -1.5e-7, 1)
         #test absorption value
-        self.assertAlmostEquals(float(abs) , 0.0741, 2)
-        self.assertAlmostEquals(float(incoh), 5.62, 2)
+        self.assertAlmostEqual(float(abs) , 0.0741, 2)
+        self.assertAlmostEqual(float(incoh), 5.62, 2)
         #Test length
-        self.assertAlmostEquals(float(length), 0.1755, 2)
+        self.assertAlmostEqual(float(length), 0.1755, 2)
         #test Cu sld
-        self.assertAlmostEquals(float(cu_real), 9.46e-6, 1)
-        self.assertAlmostEquals(float(cu_im), 3.01e-8)
+        self.assertAlmostEqual(float(cu_real), 9.46e-6, 1)
+        self.assertAlmostEqual(float(cu_im), 3.01e-8)
         # test Mo sld
-        self.assertAlmostEquals(float(mo_real), 9.43e-6)
-        self.assertAlmostEquals(float(mo_im), 5.65e-7, 1)
+        self.assertAlmostEqual(float(mo_real), 9.43e-6)
+        self.assertAlmostEqual(float(mo_im), 5.65e-7, 1)
         #compute invariant with all correct inputs value
         self.sld_frame.panel.compound_ctl.SetValue("H2O")
@@ -130,6 +130,6 @@
         bkg = self.sld_frame.panel.wavelength_ctl.GetBackgroundColour()
         value = self.sld_frame.panel.wavelength_ctl.GetValue()
-        self.assert_(bkg.GetAsString() == 'white')
-        self.assert_(float(value) == WAVELENGTH/2)
+        self.assertTrue(bkg.GetAsString() == 'white')
+        self.assertTrue(float(value) == WAVELENGTH/2)
         
     def testSomeCombination(self):
@@ -145,9 +145,9 @@
         self.sld_frame.panel.ProcessEvent(clickEvent)
         cp_bkg = self.sld_frame.panel.compound_ctl.GetBackgroundColour()
-        self.assert_(cp_bkg.GetAsString() == 'white')
+        self.assertTrue(cp_bkg.GetAsString() == 'white')
         ds_bkg = self.sld_frame.panel.density_ctl.GetBackgroundColour()
-        self.assert_(ds_bkg.GetAsString() == 'white')
+        self.assertTrue(ds_bkg.GetAsString() == 'white')
         wv_bkg = self.sld_frame.panel.wavelength_ctl.GetBackgroundColour()
-        self.assert_(wv_bkg.GetAsString() == 'pink')
+        self.assertTrue(wv_bkg.GetAsString() == 'pink')
         #density, wavelength is invalid
         self.sld_frame.panel.compound_ctl.SetValue("H2O")
@@ -158,9 +158,9 @@
         self.sld_frame.panel.ProcessEvent(clickEvent)
         cp_bkg = self.sld_frame.panel.compound_ctl.GetBackgroundColour()
-        self.assert_(cp_bkg.GetAsString() == 'white')
+        self.assertTrue(cp_bkg.GetAsString() == 'white')
         ds_bkg = self.sld_frame.panel.density_ctl.GetBackgroundColour()
-        self.assert_(ds_bkg.GetAsString() == 'pink')
+        self.assertTrue(ds_bkg.GetAsString() == 'pink')
         wv_bkg = self.sld_frame.panel.wavelength_ctl.GetBackgroundColour()
-        self.assert_(wv_bkg.GetAsString() == 'pink')
+        self.assertTrue(wv_bkg.GetAsString() == 'pink')
         #density, wavelength is invalid
         self.sld_frame.panel.compound_ctl.SetValue("invalid compound")
@@ -171,11 +171,11 @@
         self.sld_frame.panel.ProcessEvent(clickEvent)
         cp_bkg = self.sld_frame.panel.compound_ctl.GetBackgroundColour()
-        self.assert_(cp_bkg.GetAsString() == 'pink')
+        self.assertTrue(cp_bkg.GetAsString() == 'pink')
         ds_bkg = self.sld_frame.panel.density_ctl.GetBackgroundColour()
-        self.assert_(ds_bkg.GetAsString() == 'pink')
+        self.assertTrue(ds_bkg.GetAsString() == 'pink')
         wv_bkg = self.sld_frame.panel.wavelength_ctl.GetBackgroundColour()
-        self.assert_(wv_bkg.GetAsString() == 'white')
+        self.assertTrue(wv_bkg.GetAsString() == 'white')
         value = self.sld_frame.panel.wavelength_ctl.GetValue()
-        self.assert_(float(value) == WAVELENGTH)
+        self.assertTrue(float(value) == WAVELENGTH)
 
         
Index: test/pr_inversion/test/utest_explorer.py
===================================================================
--- test/pr_inversion/test/utest_explorer.py	(revision f53d684ed4a54de3601e5f4bc601f78376d98e83)
+++ test/pr_inversion/test/utest_explorer.py	(revision 1852b17578f93eff22ccdad62975a4cabdd21da7)
@@ -3,10 +3,14 @@
 """
 
+import sys
 import os.path
 import unittest, math, numpy
-from utest_invertor import load
 from sas.sascalc.pr.invertor import Invertor
 from sas.sascalc.pr.distance_explorer import DistExplorer
 
+try:
+    from utest_invertor import load
+except ImportError:
+    from .utest_invertor import load
 
 def find(filename):
@@ -36,5 +40,5 @@
         self.assertEqual(len(results.errors), 0)
         self.assertEqual(len(results.chi2), 25)
-        
+
 if __name__ == '__main__':
     unittest.main()
Index: test/sascalculator/test/utest_sld.py
===================================================================
--- test/sascalculator/test/utest_sld.py	(revision a50da8251d0d92f4c04979ecac4b78ac2c43b925)
+++ test/sascalculator/test/utest_sld.py	(revision 88d2e70bb28d8f9c855c86d94ad8ddd63154277b)
@@ -51,17 +51,17 @@
                                   molecule_formula=self.sld_formula)
         #test sld
-        self.assertAlmostEquals(sld_real * _SCALE, -5.6e-7, 1)
-        self.assertAlmostEquals(sld_im * _SCALE, 0)
+        self.assertAlmostEqual(sld_real * _SCALE, -5.6e-7, 1)
+        self.assertAlmostEqual(sld_im * _SCALE, 0)
         #test absorption value
-        self.assertAlmostEquals(abs, 0.0741, 2)
-        self.assertAlmostEquals(incoh, 5.62, 2)
+        self.assertAlmostEqual(abs, 0.0741, 2)
+        self.assertAlmostEqual(incoh, 5.62, 2)
         #Test length
-        self.assertAlmostEquals(length, 0.1755, 3)
+        self.assertAlmostEqual(length, 0.1755, 3)
         #test Cu sld
-        self.assertAlmostEquals(cu_real * _SCALE, 9.46e-6, 1)
-        self.assertAlmostEquals(cu_im * _SCALE, 3.01e-8)
+        self.assertAlmostEqual(cu_real * _SCALE, 9.46e-6, 1)
+        self.assertAlmostEqual(cu_im * _SCALE, 3.01e-8)
         # test Mo sld
-        self.assertAlmostEquals(mo_real * _SCALE, 9.43e-6)
-        self.assertAlmostEquals(mo_im * _SCALE, 5.65e-7,1)
+        self.assertAlmostEqual(mo_real * _SCALE, 9.43e-6)
+        self.assertAlmostEqual(mo_im * _SCALE, 5.65e-7,1)
 
 
@@ -91,17 +91,17 @@
                                   molecule_formula=self.sld_formula)
         #test sld
-        self.assertAlmostEquals(sld_real * _SCALE, 6.33e-6, 1)
-        self.assertAlmostEquals(sld_im * _SCALE, 0)
+        self.assertAlmostEqual(sld_real * _SCALE, 6.33e-6, 1)
+        self.assertAlmostEqual(sld_im * _SCALE, 0)
         #test absorption value
-        self.assertAlmostEquals(abs, 1.35e-4, 2)
-        self.assertAlmostEquals(incoh, 0.138, 2)
+        self.assertAlmostEqual(abs, 1.35e-4, 2)
+        self.assertAlmostEqual(incoh, 0.138, 2)
         #Test length
-        self.assertAlmostEquals(length, 1.549, 3)
+        self.assertAlmostEqual(length, 1.549, 3)
         #test Cu sld
-        self.assertAlmostEquals(cu_real * _SCALE, 9.36e-6, 1)
-        self.assertAlmostEquals(cu_im * _SCALE, 2.98e-8)
+        self.assertAlmostEqual(cu_real * _SCALE, 9.36e-6, 1)
+        self.assertAlmostEqual(cu_im * _SCALE, 2.98e-8)
         # test Mo sld
-        self.assertAlmostEquals(mo_real * _SCALE, 9.33e-6)
-        self.assertAlmostEquals(mo_im * _SCALE, 5.59e-9,1)
+        self.assertAlmostEqual(mo_real * _SCALE, 9.33e-6)
+        self.assertAlmostEqual(mo_im * _SCALE, 5.59e-9,1)
 
 
@@ -131,17 +131,17 @@
                                   molecule_formula=self.sld_formula)
         #test sld
-        self.assertAlmostEquals(sld_real * _SCALE, 1.04e-6, 1)
-        self.assertAlmostEquals(sld_im * _SCALE, -1.5e-7, 1)
+        self.assertAlmostEqual(sld_real * _SCALE, 1.04e-6, 1)
+        self.assertAlmostEqual(sld_im * _SCALE, -1.5e-7, 1)
         #test absorption value
-        self.assertAlmostEquals(abs, 180.0,0)
-        self.assertAlmostEquals(incoh, 0.0754, 2)
+        self.assertAlmostEqual(abs, 180.0,0)
+        self.assertAlmostEqual(incoh, 0.0754, 2)
         #Test length
-        self.assertAlmostEquals(length, 0.005551, 4)
+        self.assertAlmostEqual(length, 0.005551, 4)
         #test Cu sld
-        self.assertAlmostEquals(cu_real * _SCALE, 2.89e-5, 1)
-        self.assertAlmostEquals(cu_im * _SCALE, 2.81e-6)
+        self.assertAlmostEqual(cu_real * _SCALE, 2.89e-5, 1)
+        self.assertAlmostEqual(cu_im * _SCALE, 2.81e-6)
         # test Mo sld
-        self.assertAlmostEquals(mo_real * _SCALE, 2.84e-5, 1)
-        self.assertAlmostEquals(mo_im * _SCALE, 7.26e-7,1)
+        self.assertAlmostEqual(mo_real * _SCALE, 2.84e-5, 1)
+        self.assertAlmostEqual(mo_im * _SCALE, 7.26e-7,1)
 
 if __name__ == '__main__':
Index: test/sasdataloader/plugins/test_reader.py
===================================================================
--- test/sasdataloader/plugins/test_reader.py	(revision aaf5e497ff1af2aa7b8b3d361ab951e2a0ca7c93)
+++ test/sasdataloader/plugins/test_reader.py	(revision 88d2e70bb28d8f9c855c86d94ad8ddd63154277b)
@@ -40,5 +40,5 @@
                     input_f =  open(path,'r')
                 except :
-                    raise  RuntimeError, "ascii_reader: cannot open %s" % path
+                    raise  RuntimeError("ascii_reader: cannot open %s" % path)
                 buff = input_f.read()
                 lines = buff.split('\n')
@@ -55,5 +55,5 @@
                 return output
         else:
-            raise RuntimeError, "%s is not a file" % path
+            raise RuntimeError("%s is not a file" % path)
         return None
     
Index: test/sasdataloader/test/utest_abs_reader.py
===================================================================
--- test/sasdataloader/test/utest_abs_reader.py	(revision d96744de9a57d30dd495ba032913ed4268252bf5)
+++ test/sasdataloader/test/utest_abs_reader.py	(revision 4cbb2f52e9c512eb469ba3ddf768c686364d3748)
@@ -357,5 +357,5 @@
         self.assertEqual(self.data.dy[0], 3)
         self.assertEqual(self.data.x[1], 0.03)
-        self.assertAlmostEquals(self.data.y[1], 1001.0)
+        self.assertAlmostEqual(self.data.y[1], 1001.0)
         self.assertEqual(self.data.dxl[1], 0.005)
         self.assertEqual(self.data.dxw[1], 0.001)
Index: test/sasinvariant/test/utest_data_handling.py
===================================================================
--- test/sasinvariant/test/utest_data_handling.py	(revision f53d684ed4a54de3601e5f4bc601f78376d98e83)
+++ test/sasinvariant/test/utest_data_handling.py	(revision 88d2e70bb28d8f9c855c86d94ad8ddd63154277b)
@@ -45,6 +45,6 @@
 
         # Test results
-        self.assertAlmostEquals(p[0], 1.0, 5)
-        self.assertAlmostEquals(p[1], 0.0, 5)
+        self.assertAlmostEqual(p[0], 1.0, 5)
+        self.assertAlmostEqual(p[1], 0.0, 5)
 
     def test_fit_linear_data_with_noise(self):
@@ -74,6 +74,6 @@
 
         # Test results
-        self.assertAlmostEquals(p[0], 1.0, 5)
-        self.assertAlmostEquals(p[1], 0.0, 5)
+        self.assertAlmostEqual(p[0], 1.0, 5)
+        self.assertAlmostEqual(p[1], 0.0, 5)
 
     def test_fit_linear_data_with_noise_and_fixed_par(self):
@@ -506,5 +506,5 @@
         for i in range(len(self.data.x)):
             value  = math.fabs(test_y[i]-self.data.y[i])/self.data.y[i]
-            self.assert_(value < 0.001)
+            self.assertTrue(value < 0.001)
             
 class TestDataExtraLowSlitGuinier(unittest.TestCase):
@@ -553,9 +553,9 @@
 
         test_y = inv._low_extrapolation_function.evaluate_model(x=self.data.x[:inv._low_extrapolation_npts])
-        self.assert_(len(test_y) == len(self.data.y[:inv._low_extrapolation_npts]))
+        self.assertTrue(len(test_y) == len(self.data.y[:inv._low_extrapolation_npts]))
         
         for i in range(inv._low_extrapolation_npts):
             value  = math.fabs(test_y[i]-self.data.y[i])/self.data.y[i]
-            self.assert_(value < 0.001)
+            self.assertTrue(value < 0.001)
             
     def test_low_data(self):
@@ -589,8 +589,8 @@
                                                npts = inv._low_extrapolation_npts) 
         test_y = data_in_range.y
-        self.assert_(len(test_y) == len(self.data.y[:inv._low_extrapolation_npts]))
+        self.assertTrue(len(test_y) == len(self.data.y[:inv._low_extrapolation_npts]))
         for i in range(inv._low_extrapolation_npts):
             value  = math.fabs(test_y[i]-self.data.y[i])/self.data.y[i]
-            self.assert_(value < 0.001)    
+            self.assertTrue(value < 0.001)
        
             
@@ -642,9 +642,9 @@
         
         test_y = inv._high_extrapolation_function.evaluate_model(x=self.data.x[start: ])
-        self.assert_(len(test_y) == len(self.data.y[start:]))
+        self.assertTrue(len(test_y) == len(self.data.y[start:]))
         
         for i in range(len(self.data.x[start:])):
             value  = math.fabs(test_y[i]-self.data.y[start+i])/self.data.y[start+i]
-            self.assert_(value < 0.001)
+            self.assertTrue(value < 0.001)
             
     def test_high_data(self):
@@ -677,8 +677,8 @@
                                                npts = inv._high_extrapolation_npts) 
         test_y = data_in_range.y
-        self.assert_(len(test_y) == len(self.data.y[start:]))
+        self.assertTrue(len(test_y) == len(self.data.y[start:]))
         temp = self.data.y[start:]
         
         for i in range(len(self.data.x[start:])):
             value  = math.fabs(test_y[i]- temp[i])/temp[i]
-            self.assert_(value < 0.001)                
+            self.assertTrue(value < 0.001)
Index: test/sasinvariant/test/utest_use_cases.py
===================================================================
--- test/sasinvariant/test/utest_use_cases.py	(revision f53d684ed4a54de3601e5f4bc601f78376d98e83)
+++ test/sasinvariant/test/utest_use_cases.py	(revision 88d2e70bb28d8f9c855c86d94ad8ddd63154277b)
@@ -39,6 +39,6 @@
 
         # Test results
-        self.assertAlmostEquals(p[0], 2.3983,3)
-        self.assertAlmostEquals(p[1], 0.87833,3)
+        self.assertAlmostEqual(p[0], 2.3983,3)
+        self.assertAlmostEqual(p[1], 0.87833,3)
 
     def test_fit_line_data_fixed(self):
@@ -54,6 +54,6 @@
 
         # Test results
-        self.assertAlmostEquals(p[0], 4)
-        self.assertAlmostEquals(p[1], -4.0676,3)
+        self.assertAlmostEqual(p[0], 4)
+        self.assertAlmostEqual(p[1], -4.0676,3)
 
 
@@ -78,6 +78,6 @@
 
         # Test results
-        self.assertAlmostEquals(p[0], 2.4727,3)
-        self.assertAlmostEquals(p[1], 0.6,3)
+        self.assertAlmostEqual(p[0], 2.4727,3)
+        self.assertAlmostEqual(p[1], 0.6,3)
 
     def test_fit_line_data_fixed_no_weight(self):
@@ -93,6 +93,6 @@
 
         # Test results
-        self.assertAlmostEquals(p[0], 4)
-        self.assertAlmostEquals(p[1], -7.8,3)
+        self.assertAlmostEqual(p[0], 4)
+        self.assertAlmostEqual(p[1], -7.8,3)
 
 
@@ -132,7 +132,7 @@
 
         # Test results
-        self.assertAlmostEquals(qstar, 7.48959e-5,2)
-        self.assertAlmostEquals(v, 0.005644689, 4)
-        self.assertAlmostEquals(s , 941.7452, 3)
+        self.assertAlmostEqual(qstar, 7.48959e-5,2)
+        self.assertAlmostEqual(v, 0.005644689, 4)
+        self.assertAlmostEqual(s , 941.7452, 3)
 
     def test_use_case_2(self):
@@ -153,7 +153,7 @@
         s, ds = inv.get_surface_with_error(contrast=2.6e-6, porod_const=2)
         # Test results
-        self.assertAlmostEquals(qstar, 7.48959e-5,2)
-        self.assertAlmostEquals(v, 0.005644689, 1)
-        self.assertAlmostEquals(s , 941.7452, 3)
+        self.assertAlmostEqual(qstar, 7.48959e-5,2)
+        self.assertAlmostEqual(v, 0.005644689, 1)
+        self.assertAlmostEqual(s , 941.7452, 3)
 
     def test_use_case_3(self):
@@ -190,7 +190,7 @@
 
         # Test results
-        self.assertAlmostEquals(qstar, 7.49e-5, 1)
-        self.assertAlmostEquals(v, 0.005648401, 4)
-        self.assertAlmostEquals(s , 941.7452, 3)
+        self.assertAlmostEqual(qstar, 7.49e-5, 1)
+        self.assertAlmostEqual(v, 0.005648401, 4)
+        self.assertAlmostEqual(s , 941.7452, 3)
 
     def test_use_case_4(self):
@@ -218,7 +218,7 @@
 
         # Test results
-        self.assertAlmostEquals(qstar, 7.49e-5,2)
-        self.assertAlmostEquals(v, 0.005952674, 3)
-        self.assertAlmostEquals(s , 941.7452, 3)
+        self.assertAlmostEqual(qstar, 7.49e-5,2)
+        self.assertAlmostEqual(v, 0.005952674, 3)
+        self.assertAlmostEqual(s , 941.7452, 3)
 
     def test_use_case_5(self):
@@ -247,7 +247,7 @@
 
         # Test results
-        self.assertAlmostEquals(qstar, 7.88981e-5,2)
-        self.assertAlmostEquals(v, 0.005952674, 3)
-        self.assertAlmostEquals(s , 941.7452, 3)
+        self.assertAlmostEqual(qstar, 7.88981e-5,2)
+        self.assertAlmostEqual(v, 0.005952674, 3)
+        self.assertAlmostEqual(s , 941.7452, 3)
 
     def test_use_case_6(self):
@@ -273,7 +273,7 @@
 
         # Test results
-        self.assertAlmostEquals(qstar, 7.49e-5,2)
-        self.assertAlmostEquals(v, 0.005952674, 3)
-        self.assertAlmostEquals(s , 941.7452, 3)
+        self.assertAlmostEqual(qstar, 7.49e-5,2)
+        self.assertAlmostEqual(v, 0.005952674, 3)
+        self.assertAlmostEqual(s , 941.7452, 3)
 
 
@@ -297,7 +297,7 @@
         s = inv.get_surface(contrast=2.6e-6, porod_const=2)
         # Test results
-        self.assertAlmostEquals(qstar, 1.361677e-3, 4)
-        self.assertAlmostEquals(v, 0.115352622, 2)
-        self.assertAlmostEquals(s , 941.7452, 3 )
+        self.assertAlmostEqual(qstar, 1.361677e-3, 4)
+        self.assertAlmostEqual(v, 0.115352622, 2)
+        self.assertAlmostEqual(s , 941.7452, 3 )
 
     def test_use_case_2(self):
@@ -315,7 +315,7 @@
         s, ds = inv.get_surface_with_error(contrast=2.6e-6, porod_const=2)
         # Test results
-        self.assertAlmostEquals(qstar, 1.361677e-3, 4)
-        self.assertAlmostEquals(v, 0.115352622, 2)
-        self.assertAlmostEquals(s , 941.7452, 3 )
+        self.assertAlmostEqual(qstar, 1.361677e-3, 4)
+        self.assertAlmostEqual(v, 0.115352622, 2)
+        self.assertAlmostEqual(s , 941.7452, 3 )
 
     def test_use_case_3(self):
@@ -336,7 +336,7 @@
 
         # Test results
-        self.assertAlmostEquals(qstar, 0.00138756,2)
-        self.assertAlmostEquals(v, 0.117226896,2)
-        self.assertAlmostEquals(s ,941.7452, 3)
+        self.assertAlmostEqual(qstar, 0.00138756,2)
+        self.assertAlmostEqual(v, 0.117226896,2)
+        self.assertAlmostEqual(s ,941.7452, 3)
 
     def test_use_case_4(self):
@@ -354,5 +354,5 @@
 
         # Test results
-        self.assertAlmostEquals(qstar, 0.0045773,2)
+        self.assertAlmostEqual(qstar, 0.0045773,2)
 
     def test_use_case_5(self):
@@ -374,5 +374,5 @@
 
         # Test results
-        self.assertAlmostEquals(qstar, 0.00460319,3)
+        self.assertAlmostEqual(qstar, 0.00460319,3)
       
   
Index: test/sasrealspace/test/utest_oriented.py
===================================================================
--- test/sasrealspace/test/utest_oriented.py	(revision 1cdbcd86f80ac59e1681b673122c51157177496f)
+++ test/sasrealspace/test/utest_oriented.py	(revision 88d2e70bb28d8f9c855c86d94ad8ddd63154277b)
@@ -59,5 +59,5 @@
         ana_val = self.ana.runXY([0.1, 0.1])
         sim_val = self.model.getIq2D(0.1, 0.1)
-        self.assert_( math.fabs(sim_val/ana_val-1.0)<0.1 )
+        self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.1 )
 
 class TestCylinderAddObject(unittest.TestCase):
@@ -101,5 +101,5 @@
         #print ana_val, sim_val, sim_val/ana_val
 
-        self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+        self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
 
 
@@ -143,5 +143,5 @@
         #print ana_val, sim_val, sim_val/ana_val
 
-        self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+        self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
 
     def testalongZ(self):
@@ -156,5 +156,5 @@
         #print ana_val, sim_val, sim_val/ana_val
 
-        self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+        self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
 
     def testalongX(self):
@@ -169,5 +169,5 @@
         #print ana_val, sim_val, sim_val/ana_val
 
-        self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+        self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
 
 class TestEllipsoid(unittest.TestCase):
@@ -213,5 +213,5 @@
         #print ana_val, sim_val, sim_val/ana_val
 
-        self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+        self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
 
     def testalongZ(self):
@@ -226,5 +226,5 @@
         #print ana_val, sim_val, sim_val/ana_val
 
-        self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+        self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
 
     def testalongY(self):
@@ -240,5 +240,5 @@
 
         try:
-            self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+            self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
         except Exception:
             print("Error", ana_val, sim_val, sim_val/ana_val)
@@ -295,5 +295,5 @@
         sim_val, err = self.canvas.getIq2DError(0.1, 0.2)
 
-        self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+        self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
 
 class TestCoreShellError(unittest.TestCase):
@@ -347,5 +347,5 @@
         sim_val, err = self.canvas.getIq2DError(0.1, 0.2)
 
-        self.assert_( math.fabs(sim_val-ana_val) < 3.0 * err )
+        self.assertTrue( math.fabs(sim_val-ana_val) < 3.0 * err )
 
 class TestRunMethods(unittest.TestCase):
@@ -392,5 +392,5 @@
 
         try:
-            self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+            self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
         except Exception:
             print("Error", ana_val, sim_val, sim_val/ana_val)
@@ -404,5 +404,5 @@
 
         try:
-            self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+            self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
         except Exception:
             print("Error", ana_val, sim_val, sim_val/ana_val)
@@ -416,5 +416,5 @@
 
         try:
-            self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+            self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
         except Exception:
             print("Error", ana_val, sim_val, sim_val/ana_val)
@@ -428,5 +428,5 @@
 
         try:
-            self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+            self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
         except Exception:
             print("Error", ana_val, sim_val, sim_val/ana_val)
@@ -471,5 +471,5 @@
         sim_val = self.model.getIq2D(0.1, 0.2)
 
-        self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+        self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
 
         # Change the radius a re-evaluate
@@ -479,5 +479,5 @@
         ana_val = self.ana.runXY([0.1, 0.2])
         sim_val = self.model.getIq2D(0.1, 0.2)
-        self.assert_( math.fabs(sim_val/ana_val-1.0)<0.05 )
+        self.assertTrue( math.fabs(sim_val/ana_val-1.0)<0.05 )
 
 
Index: test/sasrealspace/test/utest_realspace.py
===================================================================
--- test/sasrealspace/test/utest_realspace.py	(revision 1cdbcd86f80ac59e1681b673122c51157177496f)
+++ test/sasrealspace/test/utest_realspace.py	(revision 88d2e70bb28d8f9c855c86d94ad8ddd63154277b)
@@ -38,5 +38,5 @@
         self.model.add('ellipsoid','elli2')
         self.model.delete('elli2')
-        self.assert_('elli2' not in self.model.getShapeList())
+        self.assertTrue('elli2' not in self.model.getShapeList())
 
     def testsetParam(self):
@@ -81,5 +81,5 @@
         value_2 = self.canvas.getIq(0.001)
 
-        self.assert_( (value_1-value_2)/value_1 < 0.1)
+        self.assertTrue( (value_1-value_2)/value_1 < 0.1)
 
     def testSetDensityTiming(self):
@@ -99,9 +99,9 @@
         t_2 = time.time()-t_0
 
-        self.assert_( t_2 < t_1 and (t_1-t_2)/t_2 > 2)
+        self.assertTrue( t_2 < t_1 and (t_1-t_2)/t_2 > 2)
 
     def testGetParamList(self):
         """ Test GetParamList on empty canvas"""
-        self.assert_('lores_density' in self.canvas.getParamList())
+        self.assertTrue('lores_density' in self.canvas.getParamList())
         handle = self.canvas.add('sphere')
 
@@ -109,5 +109,5 @@
         """ Test GetParamList on filled canvas"""
         self.canvas.add('sphere')
-        self.assert_('lores_density' in self.canvas.getParamList())
+        self.assertTrue('lores_density' in self.canvas.getParamList())
 
     def testAdd(self):
@@ -151,8 +151,8 @@
         # THIS WILL DEPEND ON THE NUMBER OF SPACE POINTS:
         # that why we need some error analysis.
-        self.assert_( (sim_2*ana_1/sim_1 - ana_2)/ana_2 < 0.1)
+        self.assertTrue( (sim_2*ana_1/sim_1 - ana_2)/ana_2 < 0.1)
 
         # test the absolute amplitude
-        self.assert_( math.fabs(sim_2-ana_2)/ana_2 < 0.1)
+        self.assertTrue( math.fabs(sim_2-ana_2)/ana_2 < 0.1)
 
     def testGetIq2(self):
@@ -196,5 +196,5 @@
         sim_2 = self.canvas.getIq(0.01)
 
-        self.assert_((sim_2-sim_1)/sim_1<0.05)
+        self.assertTrue((sim_2-sim_1)/sim_1<0.05)
 
     def testGetIq_time(self):
@@ -216,5 +216,5 @@
         delta_2 = time.time()-t_0
 
-        self.assert_((delta_2-delta_1)/delta_1<0.05)
+        self.assertTrue((delta_2-delta_1)/delta_1<0.05)
 
 
@@ -342,5 +342,5 @@
         ana = self.sphere.run(0.05)
         val, err = self.canvas.getIqError(0.05)
-        self.assert_(math.fabs(ana-val)<2.0*err)
+        self.assertTrue(math.fabs(ana-val)<2.0*err)
 
     def testRightOrder(self):
@@ -350,5 +350,5 @@
         val, err = self.canvas.getIqError(0.05)
         #print 'right', ana, val, err
-        self.assert_(math.fabs(ana-val)/ana < 1.1)
+        self.assertTrue(math.fabs(ana-val)/ana < 1.1)
 
     def testWrongOrder(self):
@@ -365,5 +365,5 @@
         val, err = self.canvas.getIqError(0.05)
         #print 'wrong', ana, val, err
-        self.assert_(math.fabs(ana-val)/ana < 1.1)
+        self.assertTrue(math.fabs(ana-val)/ana < 1.1)
 
 
