Repository: parmoo/parmoo (language: python)

Issue:
Misleading exception when running a libE_MOOP script without mpirun/arguments
When I first tried to run the `libe_basic_ex.py` example, I thought that there was a bug:
```
% PYTHONPATH=. python examples/libe_basic_ex.py 
Traceback (most recent call last):
  File "…/parmoo/examples/libe_basic_ex.py", line 60, in <module>
    my_moop.solve(sim_max=30)
  File "…/parmoo/parmoo/extras/libe.py", line 715, in solve
    raise ValueError("Cannot run ParMOO + libE with less than 2 " +
ValueError: Cannot run ParMOO + libE with less than 2 workers -- aborting...
```
I then learned that the following works:
```
% PYTHONPATH=. python examples/libe_basic_ex.py --comms local --nworkers 32
```

The `ValueError` does not really explain what went wrong. I suggest to either raise a more descriptive exception (e.g. `RuntimeError: You need to specify the number of workers on the command line.`), or to default to #CPU many workers. If possible, I would personally prefer the latter.


Discussion / hints:

Fix patch:
diff --git a/LICENSE b/LICENSE
index f328388..15e6c28 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 BSD 3-Clause License
 
-Copyright (c) 2022, parmoo
+Copyright (c) 2022, parmoo developers
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/README.rst b/README.rst
index bfba9c7..d39e7de 100644
--- a/README.rst
+++ b/README.rst
@@ -87,13 +87,16 @@ The easiest way to install ParMOO is via the Python package index, PyPI
 
 .. code-block:: bash
 
-    pip install [--user] parmoo
+    pip install < --user > parmoo
+
+where the braces around ``< --user >`` indicate that the ``--user`` flag is
+optional.
 
 To install *all* dependencies (including libEnsemble) use:
 
 .. code-block:: bash
 
-    pip install [--user] parmoo[extras]
+    pip install < --user > "parmoo[extras]"
 
 You can also clone this project from our GitHub_ and ``pip`` install it
 in-place, so that you can easily pull the latest version or checkout
@@ -106,6 +109,20 @@ On Debian-based systems with a bash shell, this looks like:
    cd parmoo
    pip install -e .
 
+Alternatively, the latest release of ParMOO (including all required and
+optional dependencies) can be installed from the ``conda-forge`` channel using:
+
+.. code-block:: bash
+
+   conda install --channel=conda-forge parmoo
+
+Before doing so, it is recommended to create a new conda environment using:
+
+.. code-block:: bash
+
+   conda create --name channel-name
+   conda activate channel-name
+
 Testing
 -------
 
@@ -131,8 +148,12 @@ class. To get started, create a ``MOOP`` object.
 
    my_moop = MOOP(LocalGPS)
 
+To summarize the framework, in each iteration ParMOO models each simulation
+using a computationally cheap surrogate, then solves one or more scalarizations
+of the objectives, which are specified by acquisition functions.
+Read more about this framework at our ReadTheDocs_ page.
 In the above example, ``LocalGPS`` is the class of optimizers that the
-``my_moop`` will use to solve scalarized surrogate problems.
+``my_moop`` will use to solve the scalarized surrogate problems.
 
 Next, add design variables to the problem as follows using the
 ``MOOP.addDesign(*args)`` method. In this example, we define one continuous
@@ -220,6 +241,13 @@ results can be viewed using ``MOOP.getPF()`` method.
    my_moop.solve(5) # Solve with 5 iterations of ParMOO algorithm
    results = my_moop.getPF() # Extract the results
 
+After executing the above block of code, the ``results`` variable points to
+a numpy structured array, each of whose entries corresponds to a
+nondominated objective value in the ``my_moop`` object's final database.
+You can reference individual fields in the ``results`` array by using the
+``name`` keys that were assigned during ``my_moop``'s construction, or
+plot the results by using the viz_ library.
+
 Congratulations, you now know enough to get started solving MOOPs with
 ParMOO!
 
@@ -231,11 +259,13 @@ Next steps:
  * Explore the advanced examples (including a ``libEnsemble`` example)
    in the ``examples`` directory.
  * Install libEnsemble_ and get started solving MOOPs in parallel.
+ * To interactively explore your solutions, install its extra dependencies and
+   use our built-in viz_ tool.
 
 Resources
 ---------
 
-For more information, e-mail questions to:
+To seek support or report issues, e-mail:
 
  * ``parmoo@mcs.anl.gov``
 
@@ -278,3 +308,4 @@ Citing ParMOO:
 .. _Python: https://www.python.org/downloads
 .. _ReadTheDocs: https://parmoo.readthedocs.org
 .. _scipy: https://scipy.org
+.. _viz: https://parmoo.readthedocs.io/en/latest/modules/viz.html
diff --git a/docs/about.rst b/docs/about.rst
index 0810dee..cc74cde 100644
--- a/docs/about.rst
+++ b/docs/about.rst
@@ -168,6 +168,17 @@ the abstract base classes defined in ``parmoo.structs``.
 Glossary
 --------
 
+ * **Acquisition function:**
+   An *acquisition function* is our language for a family of scalarizing
+   functions, which can be used to specify targets on the Pareto front.
+   Acquisition functions may use objective scores, gradient values, and/or
+   uncertainty information in order to guide ParMOO's search for an
+   approximation to the Pareto front.
+
+    * **Ex.--** common acquisition functions from the literature
+      include weighted sums (averages) of objective values, the epsilon
+      constraint method, and expected hypervolume improvement.
+
  * **Design variable:**
    A *design variable* is an input to your simulations, which can be controlled
    within some reasonable bounds.
diff --git a/docs/index.rst b/docs/index.rst
index f4222b1..5651f34 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -16,6 +16,10 @@ ParMOO stands for "parallel multiobjective optimization".
 ParMOO can be used to solve multiobjective optimization problems (MOOPs)
 or to generate batches of simulation inputs for parallel evaluation.
 
+ParMOO is intended for scientists, engineers, optimizers, and other
+practitioners, who are looking to build or use custom solvers for
+computationally expensive multiobjective problems.
+
 If you're new to ParMOO:
 
  * Check out the :ref:`Quickstart`
diff --git a/docs/install.rst b/docs/install.rst
index e449733..fa50e2c 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -37,7 +37,10 @@ To install the latest release:
 
 .. code-block:: bash
 
-    pip install [--user] parmoo
+    pip install < --user > parmoo
+
+where the braces around ``< --user >`` indicate that the ``--user`` flag is
+optional.
 
 Note that the default install will not install the extra dependencies,
 such as libEnsemble_.
@@ -46,7 +49,7 @@ To install *all* dependencies, use:
 
 .. code-block:: bash
 
-    pip install [--user] parmoo[extras]
+    pip install < --user > parmoo[extras]
 
 To check the installation by running the full test suite, use:
 
@@ -57,6 +60,36 @@ To check the installation by running the full test suite, use:
 which will also install the test dependencies (pytest_, pytest-cov_, and
 flake8_).
 
+Conda Forge
+-----------
+
+For some users (in particular, this is the recommended method for Windows
+users), the preferred method for obtaining the latest release of
+ParMOO may be through the ``conda`` package manager.
+The latest release of ParMOO is available through the ``conda-forge`` channel.
+Note that ``conda`` does not support optional dependencies, so the following
+command will automatically fetch all required and optional dependencies:
+
+.. code-block:: bash
+
+   conda install --channel=conda-forge parmoo
+
+Before running the above command, it is recommended to create a new conda
+environment to avoid conflicts.
+Do so using:
+
+.. code-block:: bash
+
+   conda create --name channel-name
+   conda activate channel-name
+
+After performing a ``conda-forge`` installation of ParMOO, you can run
+our unit tests to make sure your installation is working:
+
+.. code-block:: bash
+
+   py.test --pyargs parmoo.tests.unit_tests
+
 Install from GitHub source
 --------------------------
 
@@ -109,7 +142,7 @@ After installation, you can run the tests using either:
 
 .. code-block:: bash
 
-    parmoo/tests/run-tests.sh -cu[rl]
+    parmoo/tests/run-tests.sh -cu<rl>
 
 
 .. _Actions: https://github.com/parmoo/parmoo/actions
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index ef66999..ecc7fc7 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -73,13 +73,16 @@ The easiest way to install ParMOO is via the Python package index, PyPI
 
 .. code-block:: bash
 
-    pip install [--user] parmoo
+    pip install < --user > parmoo
+
+where the braces around ``< --user >`` indicate that the ``--user`` flag is
+optional.
 
 To install *all* dependencies (including libEnsemble) use:
 
 .. code-block:: bash
 
-    pip install [--user] parmoo[extras]
+    pip install < --user > "parmoo[extras]"
 
 You can also clone this project from our GitHub_ and ``pip`` install it
 in-place, so that you can easily pull the latest version or checkout
@@ -92,6 +95,20 @@ On Debian-based systems with a bash shell, this looks like:
    cd parmoo
    pip install -e .
 
+Alternatively, the latest release of ParMOO (including all required and
+optional dependencies) can be installed from the ``conda-forge`` channel using:
+
+.. code-block:: bash
+
+   conda install --channel=conda-forge parmoo
+
+Before doing so, it is recommended to create a new conda environment using:
+
+.. code-block:: bash
+
+   conda create --name channel-name
+   conda activate channel-name
+
 For detailed instructions, see :doc:`install`.
 
 Testing
@@ -121,10 +138,14 @@ To get started, create a :mod:`MOOP <moop.MOOP>` object, using the
 
    my_moop = MOOP(LocalGPS)
 
+To summarize the framework, in each iteration ParMOO models each simulation
+using a computationally cheap surrogate, then solves one or more scalarizations
+of the objectives, which are specified by acquisition functions.
+Read more about this framework at our :doc:`Learn About MOOPs <about>` page.
 In the above example,
 :mod:`optimizers.LocalGPS <optimizers.gps_search.LocalGPS>`
 is the class of optimizers
-that the ``my_moop`` will use to solve scalarized surrogate problem.
+that the ``my_moop`` will use to solve the scalarized surrogate problems.
 
 Next, add design variables to the problem as follows using the
 :meth:`MOOP.addDesign(*args) <moop.MOOP.addDesign>` method.
@@ -217,6 +238,13 @@ results can be viewed using
    my_moop.solve(5) # Solve with 5 iterations of ParMOO algorithm
    results = my_moop.getPF() # Extract the results
 
+After executing the above block of code, the ``results`` variable points to
+a numpy structured array, each of whose entries corresponds to a
+nondominated objective value in the ``my_moop`` object's final database.
+You can reference individual fields in the ``results`` array by using the
+``name`` keys that were assigned during ``my_moop``'s construction, or
+plot the results by using the :doc:`viz <modules/viz>` library.
+
 Congratulations, you now know enough to get started solving MOOPs!
 
 Minimal Working Example
@@ -227,10 +255,20 @@ Putting it all together, we get the following minimal working example.
 .. literalinclude:: ../examples/quickstart.py
     :language: python
 
-The above code produces the output below.
+The above code saves all (approximate) Pareto optimal solutions in the
+``results`` variable, and prints the ``results`` variable to the standard
+output:
 
 .. literalinclude:: ../examples/quickstart.out
 
+And produces the following figure of the Pareto points:
+
+.. figure:: ../examples/Pareto\ Front.jpeg
+    :alt: Scatter plot of the Pareto front after solving demo problem
+    :align: center
+
+|
+
 Next Steps
 ----------
 
@@ -243,11 +281,13 @@ Next Steps
    :doc:`More Tutorials <tutorials/basic-tutorials>`.
  * To start solving MOOPs on parallel hardware, install libEnsemble_ and
    see the :doc:`libEnsemble tutorial <tutorials/libe-tutorial>`.
+ * To interactively explore your solutions, install its extra dependencies and
+   use our built-in :doc:`viz <modules/viz>` tool.
 
 Resources
 ---------
 
-For more information, e-mail questions to:
+To seek support or report issues, e-mail:
 
  * ``parmoo@mcs.anl.gov``
 
diff --git a/docs/refs.rst b/docs/refs.rst
index e5ec0c7..815d722 100644
--- a/docs/refs.rst
+++ b/docs/refs.rst
@@ -4,7 +4,7 @@ References
 Additional Resources
 --------------------
 
-For questions, please e-mail:
+To seek support or report issues, e-mail:
 
  * ``parmoo@mcs.anl.gov``
 
diff --git a/docs/tutorials/basic-tutorials.rst b/docs/tutorials/basic-tutorials.rst
index fd2a24d..d783a6a 100644
--- a/docs/tutorials/basic-tutorials.rst
+++ b/docs/tutorials/basic-tutorials.rst
@@ -15,10 +15,20 @@ taken from the :doc:`Quickstart <../quickstart>` guide.
 .. literalinclude:: ../../examples/quickstart.py
     :language: python
 
-The above code produces the following output.
+The above code saves all (approximate) Pareto optimal solutions in the
+``results`` variable, and prints the ``results`` variable to the standard
+output:
 
 .. literalinclude:: ../../examples/quickstart.out
 
+And produces the following figure of the Pareto points:
+
+.. figure:: ../../examples/Pareto\ Front.jpeg
+    :alt: Scatter plot of the Pareto front after solving demo problem
+    :align: center
+
+|
+
 Named Output Types
 ~~~~~~~~~~~~~~~~~~
 
diff --git a/examples/Makefile b/examples/Makefile
index ea3b1b2..e1e3b84 100644
--- a/examples/Makefile
+++ b/examples/Makefile
@@ -1,9 +1,11 @@
 OUT_FILES = quickstart.out named_var_ex.out unnamed_var_ex.out \
 			precomputed_data.out checkpointing.out advanced_ex.out \
-			libe_basic_ex.out
+			libe_basic_ex.out Pareto\ Front.jpeg
 
 all: $(OUT_FILES)
 
+Pareto Front.jpeg: quickstart.out
+
 quickstart.out: quickstart.py
 	python3 quickstart.py > quickstart.out
 
diff --git a/examples/Pareto Front.jpeg b/examples/Pareto Front.jpeg
new file mode 100644
index 0000000..a79bdbe
Binary files /dev/null and b/examples/Pareto Front.jpeg differ
diff --git a/examples/quickstart.out b/examples/quickstart.out
index 27d0db3..43bbeb8 100644
--- a/examples/quickstart.out
+++ b/examples/quickstart.out
@@ -1,21 +1,20 @@
-[(0.79468664, 'good', 3.53652197e-01, 2.82318211e-05, -0.69468664)
- (0.77417882, 'good', 3.29681323e-01, 6.66733079e-04, -0.67417882)
- (0.70493959, 'good', 2.54963989e-01, 9.03648166e-03, -0.60493959)
- (0.68736146, 'good', 2.37521197e-01, 1.26874397e-02, -0.58736146)
- (0.65122865, 'good', 2.03607296e-01, 2.21329140e-02, -0.55122865)
- (0.63560365, 'good', 1.89750542e-01, 2.70261593e-02, -0.53560365)
- (0.61411928, 'good', 1.71494776e-01, 3.45516432e-02, -0.51411928)
- (0.61216615, 'good', 1.69880937e-01, 3.52815545e-02, -0.51216615)
- (0.57847475, 'good', 1.43243133e-01, 4.90734383e-02, -0.47847475)
- (0.56529115, 'good', 1.33437626e-01, 5.50882434e-02, -0.46529115)
- (0.5379474 , 'good', 1.14208446e-01, 6.86715641e-02, -0.4379474 )
- (0.48641905, 'good', 8.20358731e-02, 9.83330112e-02, -0.38641905)
- (0.43174969, 'good', 5.37079209e-02, 1.35608288e-01, -0.33174969)
- (0.41124188, 'good', 4.46231327e-02, 1.51132874e-01, -0.31124188)
- (0.28505278, 'good', 7.23397581e-03, 2.65170637e-01, -0.18505278)
- (0.26552153, 'good', 4.29307122e-03, 2.85667232e-01, -0.16552153)
- (0.22545879, 'good', 6.48149776e-04, 3.30097607e-01, -0.12545879)
- (0.20910136, 'good', 8.28348258e-05, 3.49161198e-01, -0.10910136)
- (0.20421855, 'good', 1.77961764e-05, 3.54955534e-01, -0.10421855)
- (0.19712825, 'good', 8.24695035e-06, 3.63454347e-01, -0.09712825)] 
+[(0.72295189, 'good', 0.27347868, 0.00593641, -0.62295189)
+ (0.70976829, 'good', 0.25986371, 0.00814176, -0.60976829)
+ (0.69267845, 'good', 0.24273205, 0.01151792, -0.59267845)
+ (0.69170189, 'good', 0.24177075, 0.01172848, -0.59170189)
+ (0.654096  , 'good', 0.20620318, 0.02128798, -0.554096  )
+ (0.63163507, 'good', 0.18630883, 0.02834675, -0.53163507)
+ (0.57614278, 'good', 0.14148339, 0.05011205, -0.47614278)
+ (0.54196309, 'good', 0.11693876, 0.06658305, -0.44196309)
+ (0.52682637, 'good', 0.10681548, 0.07462383, -0.42682637)
+ (0.51559591, 'good', 0.09960078, 0.08088569, -0.41559591)
+ (0.41871823, 'good', 0.04783767, 0.14537579, -0.31871823)
+ (0.40895261, 'good', 0.04366119, 0.15291806, -0.30895261)
+ (0.36207761, 'good', 0.02626915, 0.19177602, -0.26207761)
+ (0.34753429, 'good', 0.02176637, 0.20472522, -0.24753429)
+ (0.30261242, 'good', 0.01052931, 0.24739441, -0.20261242)
+ (0.29676282, 'good', 0.00936304, 0.25324766, -0.19676282)
+ (0.29188   , 'good', 0.00844194, 0.25818593, -0.19188   )
+ (0.28309094, 'good', 0.0069041 , 0.26719498, -0.18309094)
+ (0.21793489, 'good', 0.00032166, 0.33879979, -0.11793489)] 
  dtype=[('x1', '<f8'), ('x2', '<U25'), ('f1', '<f8'), ('f2', '<f8'), ('c1', '<f8')]
diff --git a/examples/quickstart.py b/examples/quickstart.py
index 2820e99..ae628f4 100644
--- a/examples/quickstart.py
+++ b/examples/quickstart.py
@@ -41,3 +41,8 @@ def sim_func(x):
 
 # Display solution
 print(results, "\n dtype=" + str(results.dtype))
+
+# Plot results -- must have extra viz dependencies installed
+from parmoo.viz import scatter
+# The optional arg `output` exports directly to jpg instead of interactive mode
+scatter(my_moop, output="jpeg")
diff --git a/parmoo/acquisitions/epsilon_constraint.py b/parmoo/acquisitions/epsilon_constraint.py
index 6cd2898..2c3624d 100644
--- a/parmoo/acquisitions/epsilon_constraint.py
+++ b/parmoo/acquisitions/epsilon_constraint.py
@@ -12,6 +12,7 @@
 import numpy as np
 import inspect
 from parmoo.structs import AcquisitionFunction
+from parmoo.util import xerror
 
 
 class RandomConstraint(AcquisitionFunction):
@@ -48,10 +49,8 @@ def __init__(self, o, lb, ub, hyperparams):
 
         """
 
-        from parmoo.util import xerror
-
         # Check inputs
-        xerror(o, lb, ub, hyperparams)
+        xerror(o=o, lb=lb, ub=ub, hyperparams=hyperparams)
         self.o = o
         # Set the design variable count
         self.n = np.size(lb)
@@ -94,12 +93,12 @@ def setTarget(self, data, lagrange_func, history):
         no_data = False
         # Check for illegal input from data
         if not isinstance(data, dict):
-            raise ValueError("data must be a dict")
+            raise TypeError("data must be a dict")
         else:
-            if ('x_vals' in data.keys()) != ('f_vals' in data.keys()):
+            if ('x_vals' in data) != ('f_vals' in data):
                 raise AttributeError("if x_vals is a key in data, then " +
                                      "f_vals must also appear")
-            elif 'x_vals' in data.keys():
+            elif 'x_vals' in data:
                 if data['x_vals'] is not None and data['f_vals'] is not None:
                     if data['x_vals'].shape[0] != data['f_vals'].shape[0]:
                         raise ValueError("x_vals and f_vals must be equal " +
@@ -120,7 +119,7 @@ def setTarget(self, data, lagrange_func, history):
                 raise ValueError("lagrange_func() must accept exactly one"
                                  + " input")
         else:
-            raise ValueError("lagrange_func() must be callable")
+            raise TypeError("lagrange_func() must be callable")
         if no_data:
             # If data is empty, then the Pareto front is empty
             pf = {'x_vals': np.zeros((0, self.n)),
@@ -183,7 +182,7 @@ def scalarize(self, f_vals):
             if self.o != np.size(f_vals):
                 raise ValueError("f_vals must have length o")
         else:
-            raise ValueError("f_vals must be a numpy array")
+            raise TypeError("f_vals must be a numpy array")
         # Return the weighted sum of objectives, if the bounds are satisfied
         result = np.dot(f_vals, self.weights)
         for i in range(self.o):
@@ -212,13 +211,13 @@ def scalarizeGrad(self, f_vals, g_vals):
             if self.o != np.size(f_vals):
                 raise ValueError("f_vals must have length o")
         else:
-            raise ValueError("f_vals must be a numpy array")
+            raise TypeError("f_vals must be a numpy array")
         # Check that the gradient values are legal
         if isinstance(g_vals, np.ndarray):
             if self.o != g_vals.shape[0] or self.n != g_vals.shape[1]:
                 raise ValueError("g_vals must have shape o-by-n")
         else:
-            raise ValueError("g_vals must be a numpy array")
+            raise TypeError("g_vals must be a numpy array")
         # Compute the dot product between the weights and the gradient values
         result = np.dot(np.transpose(g_vals), self.weights)
         # Add the gradient of the penalty for any bound violations
diff --git a/parmoo/acquisitions/weighted_sum.py b/parmoo/acquisitions/weighted_sum.py
index 66084d5..07cccb5 100644
--- a/parmoo/acquisitions/weighted_sum.py
+++ b/parmoo/acquisitions/weighted_sum.py
@@ -13,6 +13,7 @@
 import numpy as np
 import inspect
 from parmoo.structs import AcquisitionFunction
+from parmoo.util import xerror
 
 
 class UniformWeights(AcquisitionFunction):
@@ -47,10 +48,8 @@ def __init__(self, o, lb, ub, hyperparams):
 
         """
 
-        from parmoo.util import xerror
-
         # Check inputs
-        xerror(o, lb, ub, hyperparams)
+        xerror(o=o, lb=lb, ub=ub, hyperparams=hyperparams)
         # Set the objective count
         self.o = o
         # Set the design variable count
@@ -89,12 +88,12 @@ def setTarget(self, data, lagrange_func, history):
         no_data = False
         # Check for illegal input from data
         if not isinstance(data, dict):
-            raise ValueError("data must be a dict")
+            raise TypeError("data must be a dict")
         else:
-            if ('x_vals' in data.keys()) != ('f_vals' in data.keys()):
+            if ('x_vals' in data) != ('f_vals' in data):
                 raise AttributeError("if x_vals is a key in data, then " +
                                      "f_vals must also appear")
-            elif 'x_vals' in data.keys():
+            elif 'x_vals' in data:
                 if data['x_vals'] is not None and data['f_vals'] is not None:
                     if data['x_vals'].shape[0] != data['f_vals'].shape[0]:
                         raise ValueError("x_vals and f_vals must be equal " +
@@ -115,7 +114,7 @@ def setTarget(self, data, lagrange_func, history):
                 raise ValueError("lagrange_func() must accept exactly one"
                                  + " input")
         else:
-            raise ValueError("lagrange_func() must be callable")
+            raise TypeError("lagrange_func() must be callable")
         if no_data:
             # If data is empty, then the Pareto front is empty
             pf = {'x_vals': np.zeros((0, self.n)),
@@ -162,7 +161,7 @@ def scalarize(self, f_vals):
             if self.o != np.size(f_vals):
                 raise ValueError("f_vals must have length o")
         else:
-            raise ValueError("f_vals must be a numpy array")
+            raise TypeError("f_vals must be a numpy array")
         # Compute the dot product between the weights and function values
         return np.dot(f_vals, self.weights)
 
@@ -186,13 +185,13 @@ def scalarizeGrad(self, f_vals, g_vals):
             if self.o != np.size(f_vals):
                 raise ValueError("f_vals must have length o")
         else:
-            raise ValueError("f_vals must be a numpy array")
+            raise TypeError("f_vals must be a numpy array")
         # Check that the gradient values are legal
         if isinstance(g_vals, np.ndarray):
             if self.o != g_vals.shape[0] or self.n != g_vals.shape[1]:
                 raise ValueError("g_vals must have shape o-by-n")
         else:
-            raise ValueError("g_vals must be a numpy array")
+            raise TypeError("g_vals must be a numpy array")
         # Compute the dot product between the weights and the gradient values
         return np.dot(np.transpose(g_vals), self.weights)
 
@@ -231,10 +230,8 @@ def __init__(self, o, lb, ub, hyperparams):
 
         """
 
-        from parmoo.util import xerror
-
         # Check inputs
-        xerror(o, lb, ub, hyperparams)
+        xerror(o=o, lb=lb, ub=ub, hyperparams=hyperparams)
         # Set the objective count
         self.o = o
         # Set the design variable count
@@ -243,10 +240,10 @@ def __init__(self, o, lb, ub, hyperparams):
         self.lb = lb
         self.ub = ub
         # Check the hyperparams dictionary for weights
-        if 'weights' in hyperparams.keys():
+        if 'weights' in hyperparams:
             # If weights are provided, check that they are legal
             if not isinstance(hyperparams['weights'], np.ndarray):
-                raise ValueError("when present, 'weights' must be a " +
+                raise TypeError("when present, 'weights' must be a " +
                                  "numpy array")
             else:
                 if hyperparams['weights'].size != self.o:
@@ -286,12 +283,12 @@ def setTarget(self, data, lagrange_func, history):
         no_data = False
         # Check for illegal input from data
         if not isinstance(data, dict):
-            raise ValueError("data must be a dict")
+            raise TypeError("data must be a dict")
         else:
-            if ('x_vals' in data.keys()) != ('f_vals' in data.keys()):
+            if ('x_vals' in data) != ('f_vals' in data):
                 raise AttributeError("if x_vals is a key in data, then " +
                                      "f_vals must also appear")
-            elif 'x_vals' in data.keys():
+            elif 'x_vals' in data:
                 if data['x_vals'] is not None and data['f_vals'] is not None:
                     if data['x_vals'].shape[0] != data['f_vals'].shape[0]:
                         raise ValueError("x_vals and f_vals must be equal " +
@@ -312,7 +309,7 @@ def setTarget(self, data, lagrange_func, history):
                 raise ValueError("lagrange_func() must accept exactly one"
                                  + " input")
         else:
-            raise ValueError("lagrange_func() must be callable")
+            raise TypeError("lagrange_func() must be callable")
         if no_data:
             # If data is empty, then the Pareto front is empty
             pf = {'x_vals': np.zeros((0, self.n)),
@@ -356,7 +353,7 @@ def scalarize(self, f_vals):
             if self.o != np.size(f_vals):
                 raise ValueError("f_vals must have length o")
         else:
-            raise ValueError("f_vals must be a numpy array")
+            raise TypeError("f_vals must be a numpy array")
         # Compute the dot product between the weights and function values
         return np.dot(f_vals, self.weights)
 
@@ -380,12 +377,12 @@ def scalarizeGrad(self, f_vals, g_vals):
             if self.o != np.size(f_vals):
                 raise ValueError("f_vals must have length o")
         else:
-            raise ValueError("f_vals must be a numpy array")
+            raise TypeError("f_vals must be a numpy array")
         # Check that the gradient values are legal
         if isinstance(g_vals, np.ndarray):
             if self.o != g_vals.shape[0] or self.n != g_vals.shape[1]:
                 raise ValueError("g_vals must have shape o-by-n")
         else:
-            raise ValueError("g_vals must be a numpy array")
+            raise TypeError("g_vals must be a numpy array")
         # Compute the dot product between the weights and the gradient values
         return np.dot(np.transpose(g_vals), self.weights)
diff --git a/parmoo/constraints/const_func.py b/parmoo/constraints/const_func.py
index 7ae6a05..272c330 100644
--- a/parmoo/constraints/const_func.py
+++ b/parmoo/constraints/const_func.py
@@ -5,7 +5,7 @@
 """
 
 import numpy as np
-from abc import ABC
+from abc import ABC, abstractmethod
 
 
 class const_func(ABC):
@@ -87,6 +87,7 @@ def __init__(self, des, sim):
             raise ValueError("An illegal sim_type was given")
         return
 
+    @abstractmethod
     def __call__(self, x, sim, der=0):
         """ Make const_func objects callable.
 
@@ -98,5 +99,3 @@ def __call__(self, x, sim, der=0):
             float: The constraint violation for the input x.
 
         """
-
-        raise NotImplementedError("This method has not yet been implemented")
diff --git a/parmoo/extras/libe.py b/parmoo/extras/libe.py
index c0dcb22..fd321ad 100644
--- a/parmoo/extras/libe.py
+++ b/parmoo/extras/libe.py
@@ -46,7 +46,7 @@ def parmoo_persis_gen(H, persis_info, gen_specs, libE_info):
     from libensemble.tools.persistent_support import PersistentSupport
 
     # Get moop from pers_info
-    if 'moop' in persis_info.keys():
+    if 'moop' in persis_info:
         moop = persis_info['moop']
         if not isinstance(moop, MOOP):
             raise TypeError("persis_info['moop'] must be an instance of " +
@@ -713,7 +713,10 @@ def solve(self, sim_max=200, wt_max=3600, profile=False):
 
         if nworkers < 2:
             raise ValueError("Cannot run ParMOO + libE with less than 2 " +
-                             "workers -- aborting...")
+                             "workers -- aborting...\n\n" +
+                             "Note: this error could be caused by a " +
+                             "failure to specify the communication mode " +
+                             " (e.g., local comms or MPI)")
 
         # Get the max m for all SimGroups
         max_m = max(self.moop.m)
diff --git a/parmoo/moop.py b/parmoo/moop.py
index d885e66..17ae077 100644
--- a/parmoo/moop.py
+++ b/parmoo/moop.py
@@ -506,10 +506,12 @@ def __init__(self, opt_func, hyperparams=None):
         try:
             # Try initializing the optimizer, to check that it can be done
             opt = opt_func(1, np.zeros(1), np.ones(1), self.hyperparams)
-            assert(isinstance(opt, structs.SurrogateOptimizer))
         except BaseException:
             raise TypeError("opt_func must be a derivative of the "
                             + "SurrogateOptimizer abstract class")
+        if not isinstance(opt, structs.SurrogateOptimizer):
+            raise TypeError("opt_func must be a derivative of the "
+                            + "SurrogateOptimizer abstract class")
         self.optimizer = opt_func
         return
 
@@ -580,13 +582,13 @@ def addDesign(self, *args):
             if not isinstance(arg, dict):
                 raise TypeError("Each argument must be a Python dict")
             # Check for design variable type
-            if 'des_type' in arg.keys():
+            if 'des_type' in arg:
                 if not isinstance(arg['des_type'], str):
                     raise TypeError("args['des_type'] must be a str")
             # Append a new continuous design variable (default) to the list
-            if 'des_type' not in arg.keys() or \
+            if 'des_type' not in arg or \
                arg['des_type'] in ["continuous", "cont", "real"]:
-                if 'des_tol' in arg.keys():
+                if 'des_tol' in arg:
                     if isinstance(arg['des_tol'], float):
                         if arg['des_tol'] > 0.0:
                             des_tol = arg['des_tol']
@@ -597,7 +599,7 @@ def addDesign(self, *args):
                         raise TypeError("args['des_tol'] must be a float")
                 else:
                     des_tol = 1.0e-8
-                if 'lb' in arg.keys() and 'ub' in arg.keys():
+                if 'lb' in arg and 'ub' in arg:
                     if not (isinstance(arg['lb'], float) and
                             isinstance(arg['ub'], float)):
                         raise TypeError("args['lb'] and args['ub'] must "
@@ -610,7 +612,7 @@ def addDesign(self, *args):
                     raise AttributeError("'lb' and 'ub' keys must be "
                                          + "present when 'des_type' is "
                                          + "'continuous'")
-                if 'name' in arg.keys():
+                if 'name' in arg:
                     if isinstance(arg['name'], str):
                         if any([arg['name'] == dname[0]
                                 for dname in self.des_names]):
@@ -636,7 +638,7 @@ def addDesign(self, *args):
                 self.cont_ub.append(arg['ub'])
             # Append a new categorical design variable to the list
             elif arg['des_type'] in ["categorical", "cat"]:
-                if 'levels' in arg.keys():
+                if 'levels' in arg:
                     if isinstance(arg['levels'], int):
                         if arg['levels'] < 2:
                             raise ValueError("args['levels'] must be at "
@@ -655,7 +657,7 @@ def addDesign(self, *args):
                 else:
                     raise AttributeError("'levels' must be present when "
                                          + "'des_type' is 'categorical'")
-                if 'name' in arg.keys():
+                if 'name' in arg:
                     if not isinstance(arg['name'], str):
                         raise TypeError("When present, 'name' must be a "
                                         + "str type")
@@ -677,19 +679,19 @@ def addDesign(self, *args):
                 if isinstance(arg['levels'], int):
                     self.n_lvls.append(arg['levels'])
                     self.cat_names.append([])
-                    if 'name' in arg.keys():
+                    if 'name' in arg:
                         self.des_names.append((arg['name'], 'i4'))
                 else:
                     self.n_lvls.append(len(arg['levels']))
                     self.cat_names.append(arg['levels'])
-                    if 'name' in arg.keys():
+                    if 'name' in arg:
                         self.des_names.append((arg['name'], 'U25'))
                 self.__generate_encoding__()
             # Add an integer design variable
             elif arg['des_type'] in ["integer", "int"]:
                 # Relax to a continuous design variable with des_tol = 0.5
                 des_tol = 0.5
-                if 'lb' in arg.keys() and 'ub' in arg.keys():
+                if 'lb' in arg and 'ub' in arg:
                     if not (isinstance(arg['lb'], int) and
                             isinstance(arg['ub'], int)):
                         raise TypeError("args['lb'] and args['ub'] must "
@@ -702,7 +704,7 @@ def addDesign(self, *args):
                     raise AttributeError("'lb' and 'ub' keys must be "
                                          + "present when 'des_type' is "
                                          + "'integer'")
-                if 'name' in arg.keys():
+                if 'name' in arg:
                     if isinstance(arg['name'], str):
                         if any([arg['name'] == dname[0]
                                 for dname in self.des_names]):
@@ -728,7 +730,7 @@ def addDesign(self, *args):
                 self.int_ub.append(arg['ub'])
             # Append a new custom design variable to the list
             elif arg['des_type'] in ["custom"]:
-                if 'embedding_size' in arg.keys():
+                if 'embedding_size' in arg:
                     if isinstance(arg['embedding_size'], int):
                         if arg['embedding_size'] < 1:
                             raise ValueError("args['embedding_size'] must"
@@ -739,18 +741,18 @@ def addDesign(self, *args):
                 else:
                     raise AttributeError("'embedding_size' must be present"
                                          + " when 'des_type' is 'custom'")
-                if 'dtype' in arg.keys():
+                if 'dtype' in arg:
                     if not isinstance(arg['dtype'], str):
                         raise TypeError("When present, 'dtype' must be a " +
                                         "str type")
                     else:
                         # Make sure this is a legal numpy dtype
                         np.dtype(arg['dtype'])
-                if 'name' in arg.keys():
+                if 'name' in arg:
                     if not isinstance(arg['name'], str):
                         raise TypeError("When present, 'name' must be a "
                                         + "str type")
-                    if 'dtype' in arg.keys():
+                    if 'dtype' in arg:
                         self.des_names.append((arg['name'], arg['dtype']))
                     else:
                         self.des_names.append((arg['name'], 'U25'))
@@ -760,14 +762,14 @@ def addDesign(self, *args):
                                      + self.n_custom + self.n_raw + 1)
                     self.des_names.append((name, 'f8', ))
                 # Load the custom embedder/extracter
-                if 'embedder' in arg.keys():
+                if 'embedder' in arg:
                     if not callable(arg['embedder']):
                         raise TypeError("'embedder' must be a "
                                         + "callable object")
                 else:
                     raise AttributeError("'embedder' must be present"
                                          + " when 'des_type' is 'custom'")
-                if 'extracter' in arg.keys():
+                if 'extracter' in arg:
                     if not callable(arg['extracter']):
                         raise TypeError("'extracter' must be a "
                                         + "callable object")
@@ -791,7 +793,7 @@ def addDesign(self, *args):
                     self.custom_des_tols.append(1.0e-8)
             # Append a new raw design variable to the list
             elif arg['des_type'] in ["raw"]:
-                if 'name' in arg.keys():
+                if 'name' in arg:
                     if not isinstance(arg['name'], str):
                         raise TypeError("When present, 'name' must be a "
                                         + "str type")
@@ -894,7 +896,7 @@ def addSimulation(self, *args):
             # Use the number of sims
             m = arg['m']
             # Keep track of simulation names
-            if 'name' in arg.keys():
+            if 'name' in arg:
                 if any([arg['name'] == dname[0] for dname in self.sim_names]):
                     raise ValueError("arg['name'] must be unique")
                 if m > 1:
@@ -911,7 +913,7 @@ def addSimulation(self, *args):
             self.m.append(m)
             self.m_total += m
             # Get the hyperparameter dictionary
-            if 'hyperparams' in arg.keys():
+            if 'hyperparams' in arg:
                 hyperparams = arg['hyperparams']
             else:
                 hyperparams = {}
@@ -929,7 +931,7 @@ def addSimulation(self, *args):
             # Get the simulation function
             self.sim_funcs.append(arg['sim_func'])
             # Get the starting database, if present
-            if 'sim_db' in arg.keys():
+            if 'sim_db' in arg:
                 if 'x_vals' in arg['sim_db'] and \
                    's_vals' in arg['sim_db']:
                     # If x_vals and s_vals are present, cast to np.ndarray
@@ -998,7 +1000,7 @@ def addObjective(self, *args):
         for arg in args:
             if not isinstance(arg, dict):
                 raise TypeError("Each arg must be a Python dict")
-            if 'obj_func' in arg.keys():
+            if 'obj_func' in arg:
                 if callable(arg['obj_func']):
                     if not (len(inspect.signature(arg['obj_func']).parameters)
                             == 2 or
@@ -1013,7 +1015,7 @@ def addObjective(self, *args):
                 raise AttributeError("The 'obj_func' field must be "
                                      + "present in each arg")
             # Add the objective name
-            if 'name' in arg.keys():
+            if 'name' in arg:
                 if not isinstance(arg['name'], str):
                     raise TypeError("When present, 'name' must be a string")
                 else:
@@ -1067,7 +1069,7 @@ def addConstraint(self, *args):
         for arg in args:
             if not isinstance(arg, dict):
                 raise TypeError("Each arg must be a Python dict")
-            if 'constraint' in arg.keys():
+            if 'constraint' in arg:
                 if callable(arg['constraint']):
                     if not (len(inspect.signature(arg['constraint']).
                                 parameters) == 2 or
@@ -1082,7 +1084,7 @@ def addConstraint(self, *args):
                 raise AttributeError("The 'constraint' field must be "
                                      + "present in each arg")
             # Add the constraint name
-            if 'name' in arg.keys():
+            if 'name' in arg:
                 if not isinstance(arg['name'], str):
                     raise TypeError("When present, 'name' must be a string")
                 else:
@@ -1126,10 +1128,10 @@ def addAcquisition(self, *args):
                                  + "objectives")
             if not isinstance(arg, dict):
                 raise TypeError("Every arg must be a Python dict")
-            if 'acquisition' not in arg.keys():
+            if 'acquisition' not in arg:
                 raise AttributeError("'acquisition' field must be present in "
                                      + "every arg")
-            if 'hyperparams' in arg.keys():
+            if 'hyperparams' in arg:
                 if not isinstance(arg['hyperparams'], dict):
                     raise TypeError("When present, 'hyperparams' must be a "
                                     + "Python dict")
@@ -1141,10 +1143,12 @@ def addAcquisition(self, *args):
                                                  self.scaled_lb,
                                                  self.scaled_ub,
                                                  hyperparams)
-                assert(isinstance(acquisition, structs.AcquisitionFunction))
             except BaseException:
                 raise TypeError("'acquisition' must specify a child of the"
                                 + " AcquisitionFunction class")
+            if not isinstance(acquisition, structs.AcquisitionFunction):
+                raise TypeError("'acquisition' must specify a child of the"
+                                + " AcquisitionFunction class")
             # If all checks passed, add the acquisition to the list
             self.acquisitions.append(acquisition)
         return
@@ -1481,7 +1485,7 @@ def evaluateSurrogates(self, x):
             if x.shape[0] != self.n:
                 raise ValueError("x must have length n")
         else:
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         # Evaluate the surrogate models to approximate the simulation outputs
         sim = np.zeros(self.m_total)
         m_count = 0
@@ -1515,7 +1519,7 @@ def evaluateConstraints(self, x):
             if x.shape[0] != self.n:
                 raise ValueError("x must have length n")
         else:
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         # Special case if there are no constraints, just return [0]
         if self.p == 0:
             return np.zeros(1)
@@ -1555,7 +1559,7 @@ def evaluatePenalty(self, x):
             if x.shape[0] != self.n:
                 raise ValueError("x must have length n")
         else:
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         # Evaluate the surrogate models to approximate the simulation outputs
         sim = np.zeros(self.m_total)
         m_count = 0
@@ -1599,7 +1603,7 @@ def evaluateGradients(self, x):
             if x.shape[0] != self.n:
                 raise ValueError("x must have length n")
         else:
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         # Evaluate the surrogate models to approximate the simulation outputs
         sim = np.zeros(self.m_total)
         m_count = 0
@@ -2028,7 +2032,7 @@ def solve(self, budget):
             if budget < 0:
                 raise ValueError("budget must be nonnegative")
         else:
-            raise ValueError("budget must be an int type")
+            raise TypeError("budget must be an int type")
 
         # Print logging info summary of problem setup
         logging.info(" Beginning new run of ParMOO...")
@@ -2398,11 +2402,11 @@ def save(self, filename="parmoo"):
             parmoo_state['mean'] = self.mean
         # Serialize internal databases
         parmoo_state['data'] = {}
-        if 'x_vals' in self.data.keys():
+        if 'x_vals' in self.data:
             parmoo_state['data']['x_vals'] = self.data['x_vals'].tolist()
-        if 'f_vals' in self.data.keys():
+        if 'f_vals' in self.data:
             parmoo_state['data']['f_vals'] = self.data['f_vals'].tolist()
-        if 'c_vals' in self.data.keys():
+        if 'c_vals' in self.data:
             parmoo_state['data']['c_vals'] = self.data['c_vals'].tolist()
         parmoo_state['sim_db'] = []
         for dbi in self.sim_db:
@@ -2577,11 +2581,11 @@ def load(self, filename="parmoo"):
         self.mean = np.array(parmoo_state['mean'])
         # Reload serialized internal databases
         self.data = {}
-        if 'x_vals' in parmoo_state['data'].keys():
+        if 'x_vals' in parmoo_state['data']:
             self.data['x_vals'] = np.array(parmoo_state['data']['x_vals'])
-        if 'f_vals' in parmoo_state['data'].keys():
+        if 'f_vals' in parmoo_state['data']:
             self.data['f_vals'] = np.array(parmoo_state['data']['f_vals'])
-        if 'c_vals' in parmoo_state['data'].keys():
+        if 'c_vals' in parmoo_state['data']:
             self.data['c_vals'] = np.array(parmoo_state['data']['c_vals'])
         self.sim_db = []
         for dbi in parmoo_state['sim_db']:
diff --git a/parmoo/objectives/obj_func.py b/parmoo/objectives/obj_func.py
index bf490fb..6404eba 100644
--- a/parmoo/objectives/obj_func.py
+++ b/parmoo/objectives/obj_func.py
@@ -5,7 +5,7 @@
 """
 
 import numpy as np
-from abc import ABC
+from abc import ABC, abstractmethod
 
 
 class obj_func(ABC):
@@ -87,6 +87,7 @@ def __init__(self, des, sim):
             raise ValueError("An illegal sim_type was given")
         return
 
+    @abstractmethod
     def __call__(self, x, sx, der=0):
         """ Make obj_func objects callable.
 
@@ -98,5 +99,3 @@ def __call__(self, x, sx, der=0):
             float: The output of this objective for the input x.
 
         """
-
-        raise NotImplementedError("This method has not yet been implemented")
diff --git a/parmoo/optimizers/gps_search.py b/parmoo/optimizers/gps_search.py
index 4f767e7..3070f18 100644
--- a/parmoo/optimizers/gps_search.py
+++ b/parmoo/optimizers/gps_search.py
@@ -16,6 +16,7 @@
 import numpy as np
 import inspect
 from parmoo.structs import SurrogateOptimizer, AcquisitionFunction
+from parmoo.util import xerror
 
 
 class LocalGPS(SurrogateOptimizer):
@@ -53,10 +54,8 @@ def __init__(self, o, lb, ub, hyperparams):
 
         """
 
-        from parmoo.util import xerror
-
         # Check inputs
-        xerror(o, lb, ub, hyperparams)
+        xerror(o=o, lb=lb, ub=ub, hyperparams=hyperparams)
         self.n = lb.size
         self.lb = lb
         self.ub = ub
@@ -69,7 +68,7 @@ def __init__(self, o, lb, ub, hyperparams):
                 else:
                     self.budget = hyperparams['opt_budget']
             else:
-                raise ValueError("hyperparams['opt_budget'] "
+                raise TypeError("hyperparams['opt_budget'] "
                                  "must be an integer")
         else:
             self.budget = 10000
@@ -93,7 +92,7 @@ def setObjective(self, obj_func):
                 # Add obj_func to the problem
                 self.objectives = obj_func
         else:
-            raise ValueError("obj_func() must be callable")
+            raise TypeError("obj_func() must be callable")
         return
 
     def setReset(self, reset):
@@ -140,7 +139,7 @@ def setConstraints(self, constraint_func):
                 # Add constraint_func to the problem
                 self.constraints = constraint_func
         else:
-            raise ValueError("constraint_func() must be callable")
+            raise TypeError("constraint_func() must be callable")
         return
 
     def addAcquisition(self, *args):
@@ -155,7 +154,7 @@ def addAcquisition(self, *args):
 
         # Check for illegal inputs
         if not all([isinstance(arg, AcquisitionFunction) for arg in args]):
-            raise ValueError("Args must be instances of AcquisitionFunction")
+            raise TypeError("Args must be instances of AcquisitionFunction")
         # Append all arguments to the acquisitions list
         for arg in args:
             self.acquisitions.append(arg)
@@ -182,7 +181,7 @@ def solve(self, x):
                 raise ValueError("The rows of x must match the number " +
                                  "of acquisition functions")
         else:
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         # Check that x is feasible.
         for xj in x:
             if any(self.constraints(xj) > 0.00000001) or \
@@ -283,10 +282,8 @@ def __init__(self, o, lb, ub, hyperparams):
 
         """
 
-        from parmoo.util import xerror
-
         # Check inputs
-        xerror(o, lb, ub, hyperparams)
+        xerror(o=o, lb=lb, ub=ub, hyperparams=hyperparams)
         self.n = lb.size
         self.lb = lb
         self.ub = ub
@@ -299,7 +296,7 @@ def __init__(self, o, lb, ub, hyperparams):
                 else:
                     budget = hyperparams['opt_budget']
             else:
-                raise ValueError("hyperparams['opt_budget'] "
+                raise TypeError("hyperparams['opt_budget'] "
                                  "must be an integer")
         else:
             budget = 10000
@@ -316,7 +313,7 @@ def __init__(self, o, lb, ub, hyperparams):
                 else:
                     self.gps_budget = hyperparams['gps_budget']
             else:
-                raise ValueError("hyperparams['gps_budget'] "
+                raise TypeError("hyperparams['gps_budget'] "
                                  "must be an integer")
         else:
             self.gps_budget = int(budget / 2)
@@ -342,7 +339,7 @@ def setObjective(self, obj_func):
                 # Add obj_func to the problem
                 self.objectives = obj_func
         else:
-            raise ValueError("obj_func() must be callable")
+            raise TypeError("obj_func() must be callable")
         return
 
     def setReset(self, reset):
@@ -389,7 +386,7 @@ def setConstraints(self, constraint_func):
                 # Add constraint_func to the problem
                 self.constraints = constraint_func
         else:
-            raise ValueError("constraint_func() must be callable")
+            raise TypeError("constraint_func() must be callable")
         return
 
     def addAcquisition(self, *args):
@@ -404,7 +401,7 @@ def addAcquisition(self, *args):
 
         # Check for illegal inputs
         if not all([isinstance(arg, AcquisitionFunction) for arg in args]):
-            raise ValueError("Args must be instances of AcquisitionFunction")
+            raise TypeError("Args must be instances of AcquisitionFunction")
         # Append all arguments to the acquisitions list
         for arg in args:
             self.acquisitions.append(arg)
@@ -433,7 +430,7 @@ def solve(self, x):
                 raise ValueError("The rows of x must match the number " +
                                  "of acquisition functions")
         else:
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         # Check that x is feasible.
         for xj in x:
             if any(self.constraints(xj) > 0.00000001) or \
diff --git a/parmoo/optimizers/lbfgsb.py b/parmoo/optimizers/lbfgsb.py
index 0d9f690..2421b59 100644
--- a/parmoo/optimizers/lbfgsb.py
+++ b/parmoo/optimizers/lbfgsb.py
@@ -16,6 +16,7 @@
 import numpy as np
 import inspect
 from parmoo.structs import SurrogateOptimizer, AcquisitionFunction
+from parmoo.util import xerror
 
 
 class LBFGSB(SurrogateOptimizer):
@@ -53,10 +54,8 @@ def __init__(self, o, lb, ub, hyperparams):
 
         """
 
-        from parmoo.util import xerror
-
         # Check inputs
-        xerror(o, lb, ub, hyperparams)
+        xerror(o=o, lb=lb, ub=ub, hyperparams=hyperparams)
         self.n = lb.size
         self.bounds = np.zeros((self.n, 2))
         self.bounds[:, 0] = lb
@@ -70,7 +69,7 @@ def __init__(self, o, lb, ub, hyperparams):
                 else:
                     self.budget = hyperparams['opt_budget']
             else:
-                raise ValueError("hyperparams['opt_budget'] "
+                raise TypeError("hyperparams['opt_budget'] "
                                  "must be an integer")
         else:
             self.budget = 10000
@@ -94,7 +93,7 @@ def setObjective(self, obj_func):
                 # Add obj_func to the problem
                 self.objectives = obj_func
         else:
-            raise ValueError("obj_func() must be callable")
+            raise TypeError("obj_func() must be callable")
         return
 
     def setReset(self, reset):
@@ -126,7 +125,7 @@ def setPenalty(self, penalty_func, grad_func):
                 # Add grad_func to the problem
                 self.gradients = grad_func
         else:
-            raise ValueError("grad_func() must be callable")
+            raise TypeError("grad_func() must be callable")
         # Check whether penalty_func() has an appropriate signature
         if callable(penalty_func):
             if len(inspect.signature(penalty_func).parameters) != 1:
@@ -135,7 +134,7 @@ def setPenalty(self, penalty_func, grad_func):
                 # Add Lagrangian to the problem
                 self.penalty_func = penalty_func
         else:
-            raise ValueError("penalty_func must be callable")
+            raise TypeError("penalty_func must be callable")
         return
 
     def setConstraints(self, constraint_func):
@@ -158,7 +157,7 @@ def setConstraints(self, constraint_func):
                 # Add constraint_func to the problem
                 self.constraints = constraint_func
         else:
-            raise ValueError("constraint_func() must be callable")
+            raise TypeError("constraint_func() must be callable")
         return
 
     def addAcquisition(self, *args):
@@ -173,7 +172,7 @@ def addAcquisition(self, *args):
 
         # Check for illegal inputs
         if not all([isinstance(arg, AcquisitionFunction) for arg in args]):
-            raise ValueError("Args must be instances of AcquisitionFunction")
+            raise TypeError("Args must be instances of AcquisitionFunction")
         # Append all arguments to the acquisitions list
         for arg in args:
             self.acquisitions.append(arg)
@@ -202,7 +201,7 @@ def solve(self, x):
                 raise ValueError("The rows of x must match the number " +
                                  "of acquisition functions")
         else:
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         # Check that x is feasible.
         for xj in x:
             if np.any(xj[:] < self.bounds[:, 0]) or \
@@ -267,10 +266,8 @@ def __init__(self, o, lb, ub, hyperparams):
 
         """
 
-        from parmoo.util import xerror
-
         # Check inputs
-        xerror(o, lb, ub, hyperparams)
+        xerror(o=o, lb=lb, ub=ub, hyperparams=hyperparams)
         self.n = lb.size
         self.bounds = np.zeros((self.n, 2))
         self.bounds[:, 0] = lb
@@ -284,7 +281,7 @@ def __init__(self, o, lb, ub, hyperparams):
                 else:
                     self.budget = hyperparams['opt_budget']
             else:
-                raise ValueError("hyperparams['opt_budget'] "
+                raise TypeError("hyperparams['opt_budget'] "
                                  "must be an integer")
         else:
             self.budget = 10000
@@ -308,7 +305,7 @@ def setObjective(self, obj_func):
                 # Add obj_func to the problem
                 self.objectives = obj_func
         else:
-            raise ValueError("obj_func() must be callable")
+            raise TypeError("obj_func() must be callable")
         return
 
     def setReset(self, reset):
@@ -329,7 +326,7 @@ def setReset(self, reset):
                 # Add obj_func to the problem
                 self.resetObjectives = reset
         else:
-            raise ValueError("reset() must be callable")
+            raise TypeError("reset() must be callable")
         return
 
     def setPenalty(self, penalty_func, grad_func):
@@ -352,7 +349,7 @@ def setPenalty(self, penalty_func, grad_func):
                 # Add grad_func to the problem
                 self.gradients = grad_func
         else:
-            raise ValueError("grad_func() must be callable")
+            raise TypeError("grad_func() must be callable")
         # Check whether penalty_func() has an appropriate signature
         if callable(penalty_func):
             if len(inspect.signature(penalty_func).parameters) != 1:
@@ -361,7 +358,7 @@ def setPenalty(self, penalty_func, grad_func):
                 # Add Lagrangian to the problem
                 self.penalty_func = penalty_func
         else:
-            raise ValueError("penalty_func must be callable")
+            raise TypeError("penalty_func must be callable")
         return
 
     def setConstraints(self, constraint_func):
@@ -384,7 +381,7 @@ def setConstraints(self, constraint_func):
                 # Add constraint_func to the problem
                 self.constraints = constraint_func
         else:
-            raise ValueError("constraint_func() must be callable")
+            raise TypeError("constraint_func() must be callable")
         return
 
     def addAcquisition(self, *args):
@@ -399,7 +396,7 @@ def addAcquisition(self, *args):
 
         # Check for illegal inputs
         if not all([isinstance(arg, AcquisitionFunction) for arg in args]):
-            raise ValueError("Args must be instances of AcquisitionFunction")
+            raise TypeError("Args must be instances of AcquisitionFunction")
         # Append all arguments to the acquisitions list
         for arg in args:
             self.acquisitions.append(arg)
@@ -428,7 +425,7 @@ def solve(self, x):
                 raise ValueError("The rows of x must match the number " +
                                  "of acquisition functions")
         else:
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         # Check that x is feasible.
         for xj in x:
             if np.any(xj[:] < self.bounds[:, 0]) or \
diff --git a/parmoo/optimizers/random_search.py b/parmoo/optimizers/random_search.py
index d1ccf2d..d2552d3 100644
--- a/parmoo/optimizers/random_search.py
+++ b/parmoo/optimizers/random_search.py
@@ -15,6 +15,7 @@
 import numpy as np
 import inspect
 from parmoo.structs import SurrogateOptimizer, AcquisitionFunction
+from parmoo.util import xerror
 
 
 class RandomSearch(SurrogateOptimizer):
@@ -51,10 +52,8 @@ def __init__(self, o, lb, ub, hyperparams):
 
         """
 
-        from parmoo.util import xerror
-
         # Check inputs
-        xerror(o, lb, ub, hyperparams)
+        xerror(o=o, lb=lb, ub=ub, hyperparams=hyperparams)
         self.n = lb.size
         self.lb = lb
         self.ub = ub
@@ -67,7 +66,7 @@ def __init__(self, o, lb, ub, hyperparams):
                 else:
                     self.budget = hyperparams['opt_budget']
             else:
-                raise ValueError("hyperparams['opt_budget'] "
+                raise TypeError("hyperparams['opt_budget'] "
                                  "must be an integer")
         else:
             self.budget = 10000
@@ -92,7 +91,7 @@ def setObjective(self, obj_func):
                 # Add obj_func to the problem
                 self.objectives = obj_func
         else:
-            raise ValueError("obj_func() must be callable")
+            raise TypeError("obj_func() must be callable")
         return
 
     def setReset(self, reset):
@@ -139,7 +138,7 @@ def setConstraints(self, constraint_func):
                 # Add constraint_func to the problem
                 self.constraints = constraint_func
         else:
-            raise ValueError("constraint_func() must be callable")
+            raise TypeError("constraint_func() must be callable")
         return
 
     def addAcquisition(self, *args):
@@ -154,7 +153,7 @@ def addAcquisition(self, *args):
 
         # Check for illegal inputs
         if not all([isinstance(arg, AcquisitionFunction) for arg in args]):
-            raise ValueError("Args must be instances of AcquisitionFunction")
+            raise TypeError("Args must be instances of AcquisitionFunction")
         # Append all arguments to the acquisitions list
         for arg in args:
             self.acquisitions.append(arg)
@@ -183,7 +182,7 @@ def solve(self, x):
                 raise ValueError("The rows of x must match the number " +
                                  "of acquisition functions")
         else:
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         # Check that x is feasible.
         for xj in x:
             if any(self.constraints(xj) > 0.00000001) or \
diff --git a/parmoo/searches/latin_hypercube.py b/parmoo/searches/latin_hypercube.py
index c54cd66..298607f 100644
--- a/parmoo/searches/latin_hypercube.py
+++ b/parmoo/searches/latin_hypercube.py
@@ -12,6 +12,7 @@
 import numpy as np
 from parmoo.structs import GlobalSearch
 from pyDOE import lhs
+from parmoo.util import xerror
 
 
 class LatinHypercube(GlobalSearch):
@@ -47,10 +48,8 @@ def __init__(self, m, lb, ub, hyperparams):
 
         """
 
-        from parmoo.util import xerror
-
         # Check inputs
-        xerror(m, lb, ub, hyperparams)
+        xerror(o=m, lb=lb, ub=ub, hyperparams=hyperparams)
         self.n = lb.size
         # Assign the bounds
         self.lb = lb
@@ -86,10 +85,8 @@ def startSearch(self, lb, ub):
 
         """
 
-        from parmoo.util import xerror
-
         # Check inputs
-        xerror(1, lb, ub, {})
+        xerror(lb=lb, ub=ub)
         # Assign the bounds
         self.lb = lb
         self.ub = ub
diff --git a/parmoo/simulations/sim_func.py b/parmoo/simulations/sim_func.py
index 4b6045d..a6d11da 100644
--- a/parmoo/simulations/sim_func.py
+++ b/parmoo/simulations/sim_func.py
@@ -5,7 +5,7 @@
 """
 
 import numpy as np
-from abc import ABC
+from abc import ABC, abstractmethod
 
 
 class sim_func(ABC):
@@ -53,6 +53,7 @@ def __init__(self, des):
             raise ValueError("An illegal des_type was given")
         return
 
+    @abstractmethod
     def __call__(self, x):
         """ Make sim_func objects callable.
 
@@ -64,5 +65,3 @@ def __call__(self, x):
             numpy.ndarray: The output of this simulation for the input x.
 
         """
-
-        raise NotImplementedError("This method has not yet been implemented")
diff --git a/parmoo/structs.py b/parmoo/structs.py
index ade2e6f..d1d14e4 100644
--- a/parmoo/structs.py
+++ b/parmoo/structs.py
@@ -13,7 +13,7 @@
 
 """
 
-from abc import ABC
+from abc import ABC, abstractmethod
 
 
 class AcquisitionFunction(ABC):
@@ -28,6 +28,7 @@ class AcquisitionFunction(ABC):
 
     """
 
+    @abstractmethod
     def __init__(self, o, lb, ub, hyperparams):
         """ Constructor for the AcquisitionFunction class.
 
@@ -48,8 +49,7 @@ def __init__(self, o, lb, ub, hyperparams):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
+    @abstractmethod
     def setTarget(self, data, constraint_func, history):
         """ Set a new target value or region for the AcquisitionFunction.
 
@@ -82,8 +82,7 @@ def setTarget(self, data, constraint_func, history):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
+    @abstractmethod
     def scalarize(self, f_vals):
         """ Scalarize a vector-valued function using the AcquisitionFunction.
 
@@ -96,8 +95,6 @@ def scalarize(self, f_vals):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
     def scalarizeGrad(self, f_vals, g_vals):
         """ Scalarize a Jacobian of gradients using the current weights.
 
@@ -157,6 +154,7 @@ class GlobalSearch(ABC):
 
     """
 
+    @abstractmethod
     def __init__(self, o, lb, ub, hyperparams):
         """ Constructor for the GlobalSearch class.
 
@@ -178,8 +176,7 @@ def __init__(self, o, lb, ub, hyperparams):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
+    @abstractmethod
     def startSearch(self, lb, ub):
         """ Begin a new global search.
 
@@ -196,8 +193,6 @@ def startSearch(self, lb, ub):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
     def resumeSearch(self):
         """ Resume a global search.
 
@@ -255,6 +250,7 @@ class SurrogateFunction(ABC):
 
     """
 
+    @abstractmethod
     def __init__(self, m, lb, ub, hyperparams):
         """ Constructor for the SurrogateFunction class.
 
@@ -281,8 +277,7 @@ def __init__(self, m, lb, ub, hyperparams):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
+    @abstractmethod
     def fit(self, x, f):
         """ Fit a new surrogate to the given data.
 
@@ -295,8 +290,7 @@ def fit(self, x, f):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
+    @abstractmethod
     def update(self, x, f):
         """ Update an existing surrogate model using new data.
 
@@ -310,8 +304,7 @@ def update(self, x, f):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
+    @abstractmethod
     def setCenter(self, center):
         """ Set the center for the fit, if this is a local method.
 
@@ -321,8 +314,7 @@ def setCenter(self, center):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
+    @abstractmethod
     def evaluate(self, x):
         """ Evaluate the surrogate at a design point.
 
@@ -336,8 +328,6 @@ def evaluate(self, x):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
     def gradient(self, x):
         """ Evaluate the gradient of the surrogate at a design point.
 
@@ -353,6 +343,7 @@ def gradient(self, x):
 
         raise NotImplementedError("This class method has not been implemented")
 
+    @abstractmethod
     def improve(self, x, global_imrpov):
         """ Suggests a design to evaluate to improve the surrogate near x.
 
@@ -369,8 +360,6 @@ def improve(self, x, global_imrpov):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
     def save(self, filename):
         """ Save important data from this class so that it can be reloaded.
 
@@ -417,6 +406,7 @@ class SurrogateOptimizer(ABC):
 
     """
 
+    @abstractmethod
     def __init__(self, o, lb, ub, hyperparams):
         """ Constructor for the SurrogateOptimizer class.
 
@@ -437,8 +427,7 @@ def __init__(self, o, lb, ub, hyperparams):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
+    @abstractmethod
     def setObjective(self, obj_func):
         """ Add a vector-valued objective function that will be solved.
 
@@ -448,8 +437,6 @@ def setObjective(self, obj_func):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
     def setGradient(self, grad_func):
         """ Add a matrix-valued gradient function for obj_func.
 
@@ -461,6 +448,7 @@ def setGradient(self, grad_func):
 
         raise NotImplementedError("This class method has not been implemented")
 
+    @abstractmethod
     def setConstraints(self, constraint_func):
         """ Add a constraint function that will be satisfied.
 
@@ -472,8 +460,7 @@ def setConstraints(self, constraint_func):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
+    @abstractmethod
     def addAcquisition(self, *args):
         """ Add an acquisition function for the surrogate optimizer.
 
@@ -484,8 +471,7 @@ def addAcquisition(self, *args):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
+    @abstractmethod
     def setReset(self, reset):
         """ Add a reset function for resetting surrogate updates.
 
@@ -496,8 +482,7 @@ def setReset(self, reset):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
+    @abstractmethod
     def solve(self, x):
         """ Solve the surrogate problem.
 
@@ -511,8 +496,6 @@ def solve(self, x):
 
         """
 
-        raise NotImplementedError("This class method has not been implemented")
-
     def save(self, filename):
         """ Save important data from this class so that it can be reloaded.
 
diff --git a/parmoo/surrogates/gaussian_proc.py b/parmoo/surrogates/gaussian_proc.py
index dd844a5..386449d 100644
--- a/parmoo/surrogates/gaussian_proc.py
+++ b/parmoo/surrogates/gaussian_proc.py
@@ -13,6 +13,7 @@
 import numpy as np
 from parmoo.structs import SurrogateFunction
 from scipy.spatial.distance import cdist
+from parmoo.util import xerror
 
 
 class GaussRBF(SurrogateFunction):
@@ -52,10 +53,8 @@ def __init__(self, m, lb, ub, hyperparams):
 
         """
 
-        from parmoo.util import xerror
-
         # Check inputs
-        xerror(m, lb, ub, hyperparams)
+        xerror(o=m, lb=lb, ub=ub, hyperparams=hyperparams)
         # Initialize problem dimensions
         self.m = m
         self.lb = lb
@@ -67,7 +66,7 @@ def __init__(self, m, lb, ub, hyperparams):
         self.f_vals = np.zeros((0, self.m))
         self.weights = np.zeros((0, 0))
         # Check for the 'nugget' optional value in hyperparams
-        if 'nugget' in hyperparams.keys():
+        if 'nugget' in hyperparams:
             if isinstance(hyperparams['nugget'], float):
                 self.nugget = hyperparams['nugget']
                 if self.nugget < 0.0:
@@ -78,7 +77,7 @@ def __init__(self, m, lb, ub, hyperparams):
                                  + " value")
         else:
             self.nugget = 0.0
-        if 'des_tols' in hyperparams.keys():
+        if 'des_tols' in hyperparams:
             if isinstance(hyperparams['des_tols'], np.ndarray):
                 if hyperparams['des_tols'].size == self.n:
                     if np.all(hyperparams['des_tols'] > 0.0):
@@ -115,14 +114,14 @@ def fit(self, x, f):
 
         # Check that the x and f values are legal
         if not isinstance(x, np.ndarray):
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         if isinstance(f, np.ndarray):
             if f.shape[0] == 0:
                 raise ValueError("no data provided")
             if self.m != np.size(f[0, :]):
                 raise ValueError("each row of f must have length m")
         else:
-            raise ValueError("f must be a numpy array")
+            raise TypeError("f must be a numpy array")
         if x.shape[0] != f.shape[0]:
             raise ValueError("x and f must have equal lengths")
         # Initialize the internal database with x and f
@@ -171,14 +170,14 @@ def update(self, x, f):
 
         # Check that the x and f values are legal
         if not isinstance(x, np.ndarray):
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         if isinstance(f, np.ndarray):
             if f.shape[0] == 0:
                 return    # No new data, do nothing
             if self.m != np.size(f[0, :]):
                 raise ValueError("each row of f must have length m")
         else:
-            raise ValueError("f must be a numpy array")
+            raise TypeError("f must be a numpy array")
         if x.shape[0] != f.shape[0]:
             raise ValueError("x and f must have equal lengths")
         # Update the internal database with x and f
@@ -238,7 +237,7 @@ def evaluate(self, x):
 
         # Check that the x is legal
         if not isinstance(x, np.ndarray):
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         else:
             if x.size != self.n:
                 raise ValueError("x must have length n")
@@ -264,7 +263,7 @@ def gradient(self, x):
 
         # Check that the x is legal
         if not isinstance(x, np.ndarray):
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         else:
             if x.size != self.n:
                 raise ValueError("x must have length n")
@@ -296,7 +295,7 @@ def improve(self, x, global_improv):
 
         # Check that the x is legal
         if not isinstance(x, np.ndarray):
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         else:
             if x.size != self.n:
                 raise ValueError("x must have length n")
@@ -437,10 +436,8 @@ def __init__(self, m, lb, ub, hyperparams):
 
         """
 
-        from parmoo.util import xerror
-
         # Check inputs
-        xerror(m, lb, ub, hyperparams)
+        xerror(o=m, lb=lb, ub=ub, hyperparams=hyperparams)
         # Initialize problem dimensions
         self.m = m
         self.lb = lb
@@ -455,7 +452,7 @@ def __init__(self, m, lb, ub, hyperparams):
         self.tr_center = np.zeros(0)
         self.loc_inds = []
         # Check for the 'nugget' optional value in hyperparams
-        if 'nugget' in hyperparams.keys():
+        if 'nugget' in hyperparams:
             if isinstance(hyperparams['nugget'], float):
                 self.nugget = hyperparams['nugget']
                 if self.nugget < 0.0:
@@ -467,7 +464,7 @@ def __init__(self, m, lb, ub, hyperparams):
         else:
             self.nugget = 0.0
         # Check for the 'n_loc' optional value in hyperparams
-        if 'n_loc' in hyperparams.keys():
+        if 'n_loc' in hyperparams:
             if isinstance(hyperparams['n_loc'], int):
                 self.n_loc = hyperparams['n_loc']
                 if self.n_loc < self.n + 1:
@@ -479,7 +476,7 @@ def __init__(self, m, lb, ub, hyperparams):
         else:
             self.n_loc = self.n + 1
         # Check for 'des_tols' optional key in hyperparms
-        if 'des_tols' in hyperparams.keys():
+        if 'des_tols' in hyperparams:
             if isinstance(hyperparams['des_tols'], np.ndarray):
                 if hyperparams['des_tols'].size == self.n:
                     if np.all(hyperparams['des_tols'] > 0.0):
@@ -516,14 +513,14 @@ def fit(self, x, f):
 
         # Check that the x and f values are legal
         if not isinstance(x, np.ndarray):
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         if isinstance(f, np.ndarray):
             if f.shape[0] == 0:
                 raise ValueError("no data provided")
             if self.m != np.size(f[0, :]):
                 raise ValueError("each row of f must have length m")
         else:
-            raise ValueError("f must be a numpy array")
+            raise TypeError("f must be a numpy array")
         if x.shape[0] != f.shape[0]:
             raise ValueError("x and f must have equal lengths")
         # Initialize the internal database with x and f
@@ -548,14 +545,14 @@ def update(self, x, f):
 
         # Check that the x and f values are legal
         if not isinstance(x, np.ndarray):
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         if isinstance(f, np.ndarray):
             if f.shape[0] == 0:
                 return    # No new data, do nothing
             if self.m != np.size(f[0, :]):
                 raise ValueError("each row of f must have length m")
         else:
-            raise ValueError("f must be a numpy array")
+            raise TypeError("f must be a numpy array")
         if x.shape[0] != f.shape[0]:
             raise ValueError("x and f must have equal lengths")
         # Update the internal database with x and f
@@ -581,7 +578,7 @@ def setCenter(self, center):
 
         # Check that the center is legal
         if not isinstance(center, np.ndarray):
-            raise ValueError("center must be a numpy array")
+            raise TypeError("center must be a numpy array")
         else:
             if center.size != self.n:
                 raise ValueError("center must have length n")
@@ -639,7 +636,7 @@ def evaluate(self, x):
 
         # Check that the x is legal
         if not isinstance(x, np.ndarray):
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         else:
             if x.size != self.n:
                 raise ValueError("x must have length n")
@@ -666,7 +663,7 @@ def gradient(self, x):
 
         # Check that the x is legal
         if not isinstance(x, np.ndarray):
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         else:
             if x.size != self.n:
                 raise ValueError("x must have length n")
@@ -699,7 +696,7 @@ def improve(self, x, global_improv):
 
         # Check that the x is legal
         if not isinstance(x, np.ndarray):
-            raise ValueError("x must be a numpy array")
+            raise TypeError("x must be a numpy array")
         else:
             if x.size != self.n:
                 raise ValueError("x must have length n")
diff --git a/parmoo/util.py b/parmoo/util.py
index f04f4d0..41f326f 100644
--- a/parmoo/util.py
+++ b/parmoo/util.py
@@ -14,7 +14,7 @@
 import inspect
 
 
-def xerror(o, lb, ub, hyperparams):
+def xerror(o=1, lb=None, ub=None, hyperparams=None):
     """ Typecheck the input arguments for a class interface.
 
     Args:
@@ -31,6 +31,13 @@ def xerror(o, lb, ub, hyperparams):
 
     """
 
+    # Assign default values, if needed
+    if lb is None:
+        lb = np.zeros(1)
+    if ub is None:
+        ub = np.ones(1)
+    if hyperparams is None:
+        hyperparams = {}
     # Check the objective count
     if isinstance(o, int):
         if o < 1:
@@ -104,12 +111,12 @@ def check_sims(n, *args):
     m = 0
     for arg in args:
         if isinstance(arg, dict):
-            if 'name' in arg.keys():
+            if 'name' in arg:
                 if not isinstance(arg['name'], str):
                     raise TypeError("sims[" + str(s) + "]['name']"
                                     + " must be a string when present")
             # Check the number of sim outputs
-            if 'm' in arg.keys():
+            if 'm' in arg:
                 if isinstance(arg['m'], int):
                     if arg['m'] > 0:
                         m = arg['m']
@@ -123,13 +130,13 @@ def check_sims(n, *args):
                 raise AttributeError("sims[" + str(s)
                                      + "] is missing the key 'm'")
             # Get the hyperparameter dict
-            if 'hyperparams' in arg.keys():
+            if 'hyperparams' in arg:
                 if not isinstance(arg['hyperparams'], dict):
                     raise TypeError("sims[" + str(s)
                                     + "]: 'hyperparams'"
                                     + " key must be a dict when present")
             # Check the search technique
-            if 'search' in arg.keys():
+            if 'search' in arg:
                 try:
                     assert(isinstance(arg['search'](m, np.zeros(n),
                                                     np.ones(n), {}),
@@ -142,7 +149,7 @@ def check_sims(n, *args):
                 raise AttributeError("sims[" + str(s) + "] is missing"
                                      + " the key 'search'")
             # Check the des_tol, if present
-            if 'des_tol' in arg.keys():
+            if 'des_tol' in arg:
                 if isinstance(arg['des_tol'], float):
                     if arg['des_tol'] <= 0.0:
                         raise ValueError("sims[" + str(s)
@@ -153,7 +160,7 @@ def check_sims(n, *args):
                                     + "]['des_tol'] must"
                                     + " be a float")
             # Get the surrogate function
-            if 'surrogate' in arg.keys():
+            if 'surrogate' in arg:
                 try:
                     if not isinstance(arg['surrogate'](m, np.zeros(n),
                                                        np.ones(n), {}),
@@ -172,7 +179,7 @@ def check_sims(n, *args):
                 raise AttributeError("sims[" + str(s) + "] is missing"
                                      + " the key 'surrogate'")
             # Get the simulation function
-            if 'sim_func' in arg.keys():
+            if 'sim_func' in arg:
                 if callable(arg['sim_func']):
                     if len(inspect.signature(arg['sim_func']).parameters) \
                        != 1 and \
@@ -189,10 +196,10 @@ def check_sims(n, *args):
                 raise AttributeError("sims[" + str(s) + "] is missing"
                                      + " the key 'sim_func'")
             # Get the starting database, if present
-            if 'sim_db' in arg.keys():
+            if 'sim_db' in arg:
                 if isinstance(arg['sim_db'], dict):
-                    if 'x_vals' in arg['sim_db'].keys() and \
-                       's_vals' in arg['sim_db'].keys():
+                    if 'x_vals' in arg['sim_db'] and \
+                       's_vals' in arg['sim_db']:
                         try:
                             # Cast arg['sim_db'] contents to np.ndarrays
                             xvals = np.asarray(arg['sim_db']['x_vals'])
@@ -234,8 +241,8 @@ def check_sims(n, *args):
                                              + "]['sim_db']['s_vals']"
                                              + " is nonempty, and vice"
                                              + " versa")
-                    elif 'x_vals' in arg['sim_db'].keys() or \
-                         's_vals' in arg['sim_db'].keys():
+                    elif 'x_vals' in arg['sim_db'] or \
+                         's_vals' in arg['sim_db']:
                         raise AttributeError("sims[" + str(s) + "] cannot"
                                              + " contain a sim_db with"
                                              + " 'x_vals' but not 's_vals'"


Test patch:
diff --git a/parmoo/tests/unit_tests/test_epsilon_constraint.py b/parmoo/tests/unit_tests/test_epsilon_constraint.py
index f2bfa51..2f6baec 100644
--- a/parmoo/tests/unit_tests/test_epsilon_constraint.py
+++ b/parmoo/tests/unit_tests/test_epsilon_constraint.py
@@ -26,7 +26,7 @@ def obj_f(x):
     # Initialize a good instance
     acqu = RandomConstraint(3, np.zeros(3), np.ones(3), {})
     # Try some bad targets to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.setTarget(5, lambda x: np.zeros(1), {})
     with pytest.raises(AttributeError):
         acqu.setTarget({'x_vals': []}, lambda x: np.zeros(0), {})
@@ -39,7 +39,7 @@ def obj_f(x):
     with pytest.raises(ValueError):
         acqu.setTarget({'x_vals': np.ones((1, 3)), 'f_vals': np.ones((1, 1))},
                        lambda x: np.zeros(0), {})
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.setTarget(data, 5, {})
     with pytest.raises(ValueError):
         acqu.setTarget(data, lambda x, y: np.zeros(0), {})
@@ -58,7 +58,7 @@ def obj_f(x):
                                  {}) < 1.0))
     assert(acqu.setTarget(data, lambda x: np.zeros(3), {}) in data['x_vals'])
     # Try some bad scalarizations to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.scalarize(5)
     with pytest.raises(ValueError):
         acqu.scalarize(np.ones(2))
@@ -74,11 +74,11 @@ def obj_f(x):
         assert(acqu.scalarize(fi) <= np.sum(fi) or
                np.any(fi > acqu.f_ub))
     # Try some bad gradient scalarizations to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.scalarizeGrad(5, np.zeros((3, 4))[0])
     with pytest.raises(ValueError):
         acqu.scalarizeGrad(np.ones(2), np.zeros((3, 4)))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.scalarizeGrad(np.eye(3)[0], 5)
     with pytest.raises(ValueError):
         acqu.scalarizeGrad(np.eye(3)[0], np.zeros((2, 4)))
diff --git a/parmoo/tests/unit_tests/test_gaussian_proc.py b/parmoo/tests/unit_tests/test_gaussian_proc.py
index 59ede14..b5e433e 100644
--- a/parmoo/tests/unit_tests/test_gaussian_proc.py
+++ b/parmoo/tests/unit_tests/test_gaussian_proc.py
@@ -39,9 +39,9 @@ def test_GaussRBF():
     x_vals_full = np.concatenate((x_vals1, x_vals2), axis=0)
     y_vals_full = np.concatenate((y_vals1, y_vals2), axis=0)
     # Try to fit/update with illegal data to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.fit(0, y_vals1)
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.fit(x_vals1, 0)
     with pytest.raises(ValueError):
         rbf1.fit(np.zeros((0, 3)), np.zeros((0, 2)))
@@ -49,9 +49,9 @@ def test_GaussRBF():
         rbf1.fit(np.zeros((10, 3)), np.zeros((10, 3)))
     with pytest.raises(ValueError):
         rbf1.fit(np.zeros((10, 3)), np.zeros((9, 2)))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.update(0, y_vals1)
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.update(x_vals1, 0)
     with pytest.raises(ValueError):
         rbf1.update(np.zeros((10, 3)), np.zeros((10, 3)))
@@ -63,19 +63,19 @@ def test_GaussRBF():
     rbf1.update(np.zeros((0, 3)), np.zeros((0, 2)))    # Update with no data
     rbf2.fit(x_vals_full, y_vals_full)
     # Try a bad function/gradient evaluation to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.evaluate(5)
     with pytest.raises(ValueError):
         rbf1.evaluate(np.zeros(2))
     with pytest.raises(ValueError):
         rbf1.evaluate(-np.ones(3))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.gradient(5)
     with pytest.raises(ValueError):
         rbf1.gradient(np.zeros(2))
     with pytest.raises(ValueError):
         rbf1.gradient(-np.ones(3))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.improve(5, False)
     with pytest.raises(ValueError):
         rbf1.improve(np.zeros(2), False)
@@ -211,9 +211,9 @@ def test_LocalGaussRBF():
     x_vals_full = np.concatenate((x_vals1, x_vals2), axis=0)
     y_vals_full = np.concatenate((y_vals1, y_vals2), axis=0)
     # Try to fit/update with illegal data to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.fit(0, y_vals1)
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.fit(x_vals1, 0)
     with pytest.raises(ValueError):
         rbf1.fit(np.zeros((0, 3)), np.zeros((0, 2)))
@@ -221,9 +221,9 @@ def test_LocalGaussRBF():
         rbf1.fit(np.zeros((10, 3)), np.zeros((10, 3)))
     with pytest.raises(ValueError):
         rbf1.fit(np.zeros((10, 3)), np.zeros((9, 2)))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.update(0, y_vals1)
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.update(x_vals1, 0)
     with pytest.raises(ValueError):
         rbf1.update(np.zeros((10, 3)), np.zeros((10, 3)))
@@ -237,26 +237,26 @@ def test_LocalGaussRBF():
     rbf2.fit(x_vals_full, y_vals_full)
     rbf2.setCenter(0.5 * np.ones(3))
     # Try to set the center with illegal values to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.setCenter(5)
     with pytest.raises(ValueError):
         rbf1.setCenter(np.zeros(5))
     with pytest.raises(ValueError):
         rbf1.setCenter(-np.ones(3))
     # Try a bad function/gradient evaluation to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.evaluate(5)
     with pytest.raises(ValueError):
         rbf1.evaluate(np.zeros(2))
     with pytest.raises(ValueError):
         rbf1.evaluate(-np.ones(3))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.gradient(5)
     with pytest.raises(ValueError):
         rbf1.gradient(np.zeros(2))
     with pytest.raises(ValueError):
         rbf1.gradient(-np.ones(3))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         rbf1.improve(5, False)
     with pytest.raises(ValueError):
         rbf1.improve(np.zeros(2), False)
diff --git a/parmoo/tests/unit_tests/test_gps_search.py b/parmoo/tests/unit_tests/test_gps_search.py
index 27fa587..c57d02c 100644
--- a/parmoo/tests/unit_tests/test_gps_search.py
+++ b/parmoo/tests/unit_tests/test_gps_search.py
@@ -47,7 +47,7 @@ def g(z): return np.ones((2, 3))
     acqu3.setTarget({}, lambda x: np.zeros(2), {})
     acqu3.weights[:] = 0.5
     # Try some bad initializations to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         LocalGPS(o, lb, ub, {'opt_budget': 2.0})
     with pytest.raises(ValueError):
         LocalGPS(o, lb, ub, {'opt_budget': 0})
@@ -55,15 +55,15 @@ def g(z): return np.ones((2, 3))
     LocalGPS(o, lb, ub, {'opt_budget': 100})
     opt = LocalGPS(o, lb, ub, {})
     # Try to add some bad objectives, constraints, and acquisitions
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setObjective(5)
     with pytest.raises(ValueError):
         opt.setObjective(lambda z1, z2: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setConstraints(5)
     with pytest.raises(ValueError):
         opt.setConstraints(lambda z1, z2: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.addAcquisition(5)
     # Add the correct objective and constraints
     opt.setObjective(f)
@@ -72,7 +72,7 @@ def g(z): return np.ones((2, 3))
     opt.addAcquisition(acqu1, acqu2, acqu3)
     opt.setReset(lambda x: 100.0)
     # Try to solve with invalid inputs to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.solve(5)
     with pytest.raises(ValueError):
         opt.solve(np.zeros((3, n-1)))
@@ -146,11 +146,11 @@ def g(z): return np.ones((2, 3))
     acqu3.setTarget({}, lambda x: np.zeros(2), {})
     acqu3.weights[:] = 0.5
     # Try some bad initializations to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         GlobalGPS(o, lb, ub, {'opt_budget': 2.0})
     with pytest.raises(ValueError):
         GlobalGPS(o, lb, ub, {'opt_budget': 0})
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         GlobalGPS(o, lb, ub, {'opt_budget': 500,
                               'gps_budget': 2.0})
     with pytest.raises(ValueError):
@@ -164,15 +164,15 @@ def g(z): return np.ones((2, 3))
     GlobalGPS(o, lb, ub, {'opt_budget': 200, 'gps_budget': 100})
     opt = GlobalGPS(o, lb, ub, {})
     # Try to add some bad objectives, constraints, and acquisitions
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setObjective(5)
     with pytest.raises(ValueError):
         opt.setObjective(lambda z1, z2: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setConstraints(5)
     with pytest.raises(ValueError):
         opt.setConstraints(lambda z1, z2: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.addAcquisition(5)
     # Add the correct objective and constraints
     opt.setObjective(f)
@@ -181,7 +181,7 @@ def g(z): return np.ones((2, 3))
     opt.addAcquisition(acqu1, acqu2, acqu3)
     opt.setReset(lambda x: 100.0)
     # Try to solve with invalid inputs to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.solve(5)
     with pytest.raises(ValueError):
         opt.solve(np.zeros((3, n-1)))
diff --git a/parmoo/tests/unit_tests/test_lbfgsb.py b/parmoo/tests/unit_tests/test_lbfgsb.py
index 0057b05..3836207 100644
--- a/parmoo/tests/unit_tests/test_lbfgsb.py
+++ b/parmoo/tests/unit_tests/test_lbfgsb.py
@@ -64,7 +64,7 @@ def g(z):
     acqu3.setTarget({}, lambda x: np.zeros(2), {})
     acqu3.weights[:] = 0.5
     # Try some bad initializations to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         LBFGSB(o, lb, ub, {'opt_budget': 2.0})
     with pytest.raises(ValueError):
         LBFGSB(o, lb, ub, {'opt_budget': 0})
@@ -72,23 +72,23 @@ def g(z):
     LBFGSB(o, lb, ub, {'opt_budget': 100})
     opt = LBFGSB(o, lb, ub, {})
     # Try to add some bad objectives, constraints, and acquisitions
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setObjective(5)
     with pytest.raises(ValueError):
         opt.setObjective(lambda z1, z2: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setConstraints(5)
     with pytest.raises(ValueError):
         opt.setConstraints(lambda z1, z2: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setPenalty(5, lambda z: np.zeros(1))
     with pytest.raises(ValueError):
         opt.setPenalty(lambda z1, z2: np.zeros(1), lambda z: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setPenalty(lambda z: np.zeros(1), 5)
     with pytest.raises(ValueError):
         opt.setPenalty(lambda z: np.zeros(1), lambda z1, z2: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.addAcquisition(5)
     # Add the correct objective and constraints
     opt.setObjective(f)
@@ -97,7 +97,7 @@ def g(z):
     opt.addAcquisition(acqu1, acqu2, acqu3)
     opt.setReset(lambda x: 100.0)
     # Try to solve with invalid inputs to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.solve(5)
     with pytest.raises(ValueError):
         opt.solve(np.zeros((3, n-1)))
@@ -188,7 +188,7 @@ def g(z):
     acqu3.setTarget({}, lambda x: np.zeros(2), {})
     acqu3.weights[:] = 0.5
     # Try some bad initializations to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         TR_LBFGSB(o, lb, ub, {'opt_budget': 2.0})
     with pytest.raises(ValueError):
         TR_LBFGSB(o, lb, ub, {'opt_budget': 0})
@@ -196,25 +196,25 @@ def g(z):
     TR_LBFGSB(o, lb, ub, {'opt_budget': 100})
     opt = TR_LBFGSB(o, lb, ub, {})
     # Try to add some bad objectives, constraints, and acquisitions
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setObjective(5)
     with pytest.raises(ValueError):
         opt.setObjective(lambda z1, z2: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setConstraints(5)
     with pytest.raises(ValueError):
         opt.setConstraints(lambda z1, z2: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setPenalty(5, lambda z: np.zeros(1))
     with pytest.raises(ValueError):
         opt.setPenalty(lambda z1, z2: np.zeros(1), lambda z: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setPenalty(lambda z: np.zeros(1), 5)
     with pytest.raises(ValueError):
         opt.setPenalty(lambda z: np.zeros(1), lambda z1, z2: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.addAcquisition(5)
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setReset(5)
     with pytest.raises(ValueError):
         opt.setReset(lambda z1, z2: 0.0)
@@ -225,7 +225,7 @@ def g(z):
     opt.addAcquisition(acqu1, acqu2, acqu3)
     opt.setReset(lambda x: 100.0)
     # Try to solve with invalid inputs to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.solve(5)
     with pytest.raises(ValueError):
         opt.solve(np.zeros((3, n-1)))
diff --git a/parmoo/tests/unit_tests/test_moop.py b/parmoo/tests/unit_tests/test_moop.py
index 0943cf2..aac75ea 100644
--- a/parmoo/tests/unit_tests/test_moop.py
+++ b/parmoo/tests/unit_tests/test_moop.py
@@ -592,7 +592,7 @@ def test_MOOP_evaluateSurrogates():
     moop1.fitSurrogates()
     moop1.resetSurrogates(np.ones(3) * 0.5)
     # Now try some bad evaluations
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         moop1.evaluateSurrogates(10.0)
     with pytest.raises(ValueError):
         moop1.evaluateSurrogates(np.zeros(1))
@@ -706,7 +706,7 @@ def test_MOOP_evaluateConstraints():
     moop1.evaluateSimulation(np.ones(3), 1)
     moop1.fitSurrogates()
     # Now try some bad evaluations
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         moop1.evaluateConstraints(10.0)
     with pytest.raises(ValueError):
         moop1.evaluateConstraints(np.zeros(1))
@@ -857,7 +857,7 @@ def c2(x, s, der=0):
     assert(np.all(moop1.evaluatePenalty(np.zeros(3)) == np.zeros(1)))
     assert(np.all(moop1.evaluatePenalty(np.ones(3)) == 3.75 * np.ones(1)))
     # Now try some bad evaluations
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         moop1.evaluatePenalty(10.0)
     with pytest.raises(ValueError):
         moop1.evaluatePenalty(np.zeros(1))
@@ -977,7 +977,7 @@ def c2(x, s, der=0):
     moop1.addConstraint({'constraint': c2})
     assert(np.all(moop1.evaluateGradients(np.ones(3)) == result))
     # Now try some bad evaluations
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         moop1.evaluateGradients(10.0)
     with pytest.raises(ValueError):
         moop1.evaluateGradients(np.zeros(1))
@@ -1568,7 +1568,7 @@ def f2(x, sim): return sim[1]
     # Try to solve several invalid problems/budgets to test error handling
     with pytest.raises(ValueError):
         moop1.solve(-1)
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         moop1.solve(2.0)
     # Solve the MOOP with 6 iterations
     moop1.solve(6)
diff --git a/parmoo/tests/unit_tests/test_random_search.py b/parmoo/tests/unit_tests/test_random_search.py
index b42b60b..78fc959 100644
--- a/parmoo/tests/unit_tests/test_random_search.py
+++ b/parmoo/tests/unit_tests/test_random_search.py
@@ -47,7 +47,7 @@ def g(z): return np.ones((2, 3))
     acqu3.setTarget({}, lambda x: np.zeros(2), {})
     acqu3.weights[:] = 0.5
     # Try some bad initializations to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         RandomSearch(o, lb, ub, {'opt_budget': 2.0})
     with pytest.raises(ValueError):
         RandomSearch(o, lb, ub, {'opt_budget': 0})
@@ -55,15 +55,15 @@ def g(z): return np.ones((2, 3))
     RandomSearch(o, lb, ub, {})
     opt = RandomSearch(o, lb, ub, {'opt_budget': 10010})
     # Try to add some bad objectives, constraints, and acquisitions
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setObjective(5)
     with pytest.raises(ValueError):
         opt.setObjective(lambda z1, z2: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.setConstraints(5)
     with pytest.raises(ValueError):
         opt.setConstraints(lambda z1, z2: np.zeros(1))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.addAcquisition(5)
     # Add the correct objective and constraints
     opt.setObjective(f)
@@ -72,7 +72,7 @@ def g(z): return np.ones((2, 3))
     opt.addAcquisition(acqu1, acqu2, acqu3)
     opt.setReset(lambda x: 100.0)
     # Try to solve with invalid inputs to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         opt.solve(5)
     with pytest.raises(ValueError):
         opt.solve(np.zeros((3, n-1)))
diff --git a/parmoo/tests/unit_tests/test_structs.py b/parmoo/tests/unit_tests/test_structs.py
index 76c912d..3cd80fe 100644
--- a/parmoo/tests/unit_tests/test_structs.py
+++ b/parmoo/tests/unit_tests/test_structs.py
@@ -1,92 +1,42 @@
 
 def test_AcquisitionFunction():
-    """ Test that the AcquisitionFunction ABC raises NotImplementedError()"""
+    """ Test that the AcquisitionFunction ABC raises a TypeError"""
 
     from parmoo.structs import AcquisitionFunction
     import pytest
 
-    with pytest.raises(NotImplementedError):
+    with pytest.raises(TypeError):
         AcquisitionFunction(0, 0, 0, 0)
-    with pytest.raises(NotImplementedError):
-        AcquisitionFunction.setTarget(0, 0, 0, 0)
-    with pytest.raises(NotImplementedError):
-        AcquisitionFunction.scalarize(0, 0)
-    with pytest.raises(NotImplementedError):
-        AcquisitionFunction.scalarizeGrad(0, 0, 0)
-    with pytest.raises(NotImplementedError):
-        AcquisitionFunction.save(0, 0)
-    with pytest.raises(NotImplementedError):
-        AcquisitionFunction.load(0, 0)
 
 
 def test_GlobalSearch():
-    """ Test that the GlobalSearch ABC raises NotImplementedError()"""
+    """ Test that the GlobalSearch ABC raises a TypeError"""
 
     from parmoo.structs import GlobalSearch
     import pytest
 
-    with pytest.raises(NotImplementedError):
+    with pytest.raises(TypeError):
         GlobalSearch(0, 0, 0, 0)
-    with pytest.raises(NotImplementedError):
-        GlobalSearch.startSearch(0, 0, 0)
-    with pytest.raises(NotImplementedError):
-        GlobalSearch.resumeSearch(0)
-    with pytest.raises(NotImplementedError):
-        GlobalSearch.save(0, 0)
-    with pytest.raises(NotImplementedError):
-        GlobalSearch.load(0, 0)
 
 
 def test_SurrogateFunction():
-    """ Test that the SurrogateFunction ABC raises NotImplementedError()"""
+    """ Test that the SurrogateFunction ABC raises a TypeError"""
 
     from parmoo.structs import SurrogateFunction
     import pytest
 
-    with pytest.raises(NotImplementedError):
+    with pytest.raises(TypeError):
         SurrogateFunction(0, 0, 0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateFunction.fit(0, 0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateFunction.update(0, 0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateFunction.setCenter(0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateFunction.evaluate(0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateFunction.gradient(0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateFunction.improve(0, 0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateFunction.save(0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateFunction.load(0, 0)
 
 
 def test_SurrogateOptimizer():
-    """ Test that the SurrogateFunction ABC raises NotImplementedError()"""
+    """ Test that the SurrogateFunction ABC raises a TypeError"""
 
     from parmoo.structs import SurrogateOptimizer
     import pytest
 
-    with pytest.raises(NotImplementedError):
+    with pytest.raises(TypeError):
         SurrogateOptimizer(0, 0, 0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateOptimizer.setObjective(0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateOptimizer.setGradient(0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateOptimizer.setConstraints(0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateOptimizer.addAcquisition(0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateOptimizer.setReset(0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateOptimizer.solve(0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateOptimizer.save(0, 0)
-    with pytest.raises(NotImplementedError):
-        SurrogateOptimizer.load(0, 0)
 
 
 if __name__ == "__main__":
diff --git a/parmoo/tests/unit_tests/test_util.py b/parmoo/tests/unit_tests/test_util.py
index 9817ddd..42ab5ed 100644
--- a/parmoo/tests/unit_tests/test_util.py
+++ b/parmoo/tests/unit_tests/test_util.py
@@ -26,8 +26,9 @@ def test_xerror():
         xerror(3, np.ones(4), np.zeros(4), {})
     with pytest.raises(TypeError):
         xerror(3, np.zeros(4), np.ones(4), 5)
-    # Perform one good initialization, to confirm that it passes
-    xerror(3, np.zeros(4), np.ones(4), {})
+    # Perform two good initialization, to confirm that it passes
+    xerror(o=3, lb=np.zeros(4), ub=np.ones(4), hyperparams={})
+    xerror()
 
 
 def test_check_sims():
diff --git a/parmoo/tests/unit_tests/test_weighted_sum.py b/parmoo/tests/unit_tests/test_weighted_sum.py
index a4ded21..05ef3ee 100644
--- a/parmoo/tests/unit_tests/test_weighted_sum.py
+++ b/parmoo/tests/unit_tests/test_weighted_sum.py
@@ -15,7 +15,7 @@ def test_UniformWeights():
     acqu = UniformWeights(3, np.zeros(4), np.ones(4), {})
     assert(np.all(acqu.lb[:] == 0.0) and np.all(acqu.ub[:] == 1.0))
     # Set some bad targets to check error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.setTarget(5, lambda x: np.zeros(3), {})
     with pytest.raises(AttributeError):
         acqu.setTarget({'x_vals': []}, lambda x: np.zeros(3), {})
@@ -28,9 +28,7 @@ def test_UniformWeights():
     with pytest.raises(ValueError):
         acqu.setTarget({'x_vals': np.ones((1, 4)), 'f_vals': np.ones((1, 1))},
                        lambda x: np.zeros(3), {})
-    with pytest.raises(ValueError):
-        acqu.setTarget({}, 4, {})
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.setTarget({}, 4, {})
     with pytest.raises(ValueError):
         acqu.setTarget({}, lambda x, y: np.zeros(3), {})
@@ -39,7 +37,7 @@ def test_UniformWeights():
     assert(abs(sum(acqu.weights) - 1.0) < 0.00000001)
     assert(np.all(x0[:] <= acqu.ub) and np.all(x0[:] >= acqu.lb))
     # Try some bad scalarizations to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.scalarize(5)
     with pytest.raises(ValueError):
         acqu.scalarize(np.ones(2))
@@ -63,11 +61,11 @@ def test_UniformWeights():
     assert(np.abs(acqu3.scalarize(np.eye(3)[0]) + acqu3.scalarize(np.eye(3)[1])
                   + acqu3.scalarize(np.eye(3)[2]) - 1.0) < 0.00000001)
     # Try some bad gradient scalarizations to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.scalarizeGrad(5, np.zeros((3, 4))[0])
     with pytest.raises(ValueError):
         acqu.scalarizeGrad(np.ones(2), np.zeros((3, 4)))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.scalarizeGrad(np.eye(3)[0], 5)
     with pytest.raises(ValueError):
         acqu.scalarizeGrad(np.eye(3)[0], np.zeros((2, 4)))
@@ -94,7 +92,7 @@ def test_FixedWeights():
     import pytest
 
     # Try some bad initializations to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         FixedWeights(3, np.zeros(4), np.ones(4), {'weights': 5.0})
     with pytest.raises(ValueError):
         FixedWeights(3, np.zeros(4), np.ones(4), {'weights': np.ones(2)})
@@ -105,7 +103,7 @@ def test_FixedWeights():
     assert(np.all(acqu.lb[:] == 0.0) and np.all(acqu.ub[:] == 1.0))
     assert(np.all(acqu.weights[:] - (1.0 / 3.0) < 0.00000001))
     # Set some bad targets to check error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.setTarget(5, lambda x: np.zeros(3), {})
     with pytest.raises(AttributeError):
         acqu.setTarget({'x_vals': []}, lambda x: np.zeros(3), {})
@@ -118,9 +116,7 @@ def test_FixedWeights():
     with pytest.raises(ValueError):
         acqu.setTarget({'x_vals': np.ones((1, 4)), 'f_vals': np.ones((1, 1))},
                        lambda x: np.zeros(3), {})
-    with pytest.raises(ValueError):
-        acqu.setTarget({}, 4, {})
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.setTarget({}, 4, {})
     with pytest.raises(ValueError):
         acqu.setTarget({}, lambda x, y: np.zeros(3), {})
@@ -136,7 +132,7 @@ def test_FixedWeights():
     x0 = acqu.setTarget({}, lambda x: np.zeros(3), {})
     assert(np.all(x0[:] <= acqu.ub) and np.all(x0[:] >= acqu.lb))
     # Try some bad scalarizations to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.scalarize(5)
     with pytest.raises(ValueError):
         acqu.scalarize(np.ones(2))
@@ -144,11 +140,11 @@ def test_FixedWeights():
     assert(np.abs(acqu.scalarize(np.eye(3)[0]) + acqu.scalarize(np.eye(3)[1])
                   + acqu.scalarize(np.eye(3)[2]) - 1.0) < 0.00000001)
     # Try some bad gradient scalarizations to test error handling
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.scalarizeGrad(5, np.zeros((3, 4))[0])
     with pytest.raises(ValueError):
         acqu.scalarizeGrad(np.ones(2), np.zeros((3, 4)))
-    with pytest.raises(ValueError):
+    with pytest.raises(TypeError):
         acqu.scalarizeGrad(np.eye(3)[0], 5)
     with pytest.raises(ValueError):
         acqu.scalarizeGrad(np.eye(3)[0], np.zeros((2, 4)))