Repository: parmoo/parmoo (language: python)

Issue:
Better model-improvement steps
Fixed 2 issues:
 1.  Updated default value of ``des_tol`` hyperparameter to scale with ``ub - lb`` for all continuous variables
 2.  Model improvement regions are now given by an infty-norm box whose radius is determined by the infty-norm distance to ``n+1`` closest point, then scaled proportionally by the design tolerance in each dimension and inversely with sample standard deviation of those ``n+1`` points as calculated in each dimension

(1) is convenient since the design tolerance is applied in the rescaled space, and so the default value should be calculated accordingly.

(2) is important since we need to collect data that is orthogonal to the span of nearby samples whenever the algorithm begins to stall out.  On our test problems, this improves performance drastically, especially when structure-exploiting methods are applied.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/parmoo-ci.yml b/.github/workflows/parmoo-ci.yml
index 31d8c5e..c5e623b 100644
--- a/.github/workflows/parmoo-ci.yml
+++ b/.github/workflows/parmoo-ci.yml
@@ -13,17 +13,17 @@ jobs:
       fail-fast: false
       matrix:
         os: [ubuntu-latest, macos-latest]
-        python-version: ["3.6", "3.10"]
+        python-version: ["3.8", "3.11"]
 
     defaults:
       run:
         shell: bash
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
     - name: Set up Python version
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: "${{matrix.python-version}}"
 
diff --git a/.readthedocs.yml b/.readthedocs.yml
index ea8ed39..f7c1a69 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -15,7 +15,7 @@ formats:
 python:
     version: 3.7
     install:
-      - requirements: docs/REQUIREMENTS.txt
+      - requirements: REQUIREMENTS
       - method: pip
         path: .
         extra_requirements:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 697d24c..3764e7b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -39,3 +39,47 @@ Desired features:
  - a visualization dashboard for viewing progress interactively
  - design variable types should be a class, with embed/extract methods
    that can be called by ``MOOP.__embed__()`` and ``MOOP.__extract__()``
+
+Release 0.2.0
+-------------
+
+:Date: Feb 2, 2023
+
+Official release corresponding to accepted JOSS article.
+
+ - Added support for a wider variety of design variables (including integer
+   types), as well as support for "custom" design variables that use
+   user-provided custom embedders/extractors
+   Documentation on design variables has been expanded accordingly.
+   Although design variables are still specified through dicts not classes,
+   this addresses and therefore closes the primary issue raised in
+   parmoo/parmoo#28
+ - Updated ``extras/libe.py`` corresponding to interface changes made in
+   libEnsemble Release 0.8.0. This also addresses the issues on MacOS,
+   referenced in parmoo/parmoo#34
+ - Added a post-run visualization library and corresponding
+   documentation, closing issue parmoo/parmoo#27
+ - Allow solvers to start from an initial point that is infeasible, so that
+   problems with relaxable constraints and a very small feasible set can
+   still be solved
+ - Various style changes and additional usage environments requested by
+   JOSS reviewers openjournals/joss-reviews#4468 including parmoo/parmoo#32
+ - Added support for multistarting optimization solvers when solving
+   surrogate problems. This is particularly important for the global
+   ``GaussRBF`` surrogate
+ - Fixed an issue in how model improvement points are calculated, as
+   implemented in the ``surrogate.improve`` method for each GaussRBF variation
+   in ``surrogates/gaussian_proc.py``, which was created when adding support
+   for custom design variables
+ - The default design tolerance for continuous variables now depends upon
+   the value of ``ub - lb``
+
+Note: 
+
+ - Dropped support for Python 3.6, due to changes to GitHub Actions documented
+   on actions/setup-python#544
+
+Known issues:
+
+ - The visualization library uses advanced plotly/dash features, which may
+   not support the chrome browser, as described in parmoo/parmoo#37
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 f60d757..ae7d32b 100644
--- a/README.rst
+++ b/README.rst
@@ -66,12 +66,19 @@ ParMOO's base has the following dependencies:
  * numpy_ -- for data structures and performant numerical linear algebra
  * scipy_ -- for scientific calculations needed for specific modules
  * pyDOE_ -- for generating experimental designs
+ * pandas_ -- for exporting the resulting databases
 
 Additional dependencies are needed to use the additional features in
 ``parmoo.extras``:
 
  * libEnsemble_ -- for managing parallel simulation evaluations
 
+And for using the Pareto front visualization library in ``parmoo.viz``:
+
+ * plotly_ -- for generating interactive plots
+ * dash_ -- for hosting interactive plots in your browser
+ * kaleido_ -- for exporting static plots post-interaction
+
 Installation
 ------------
 
@@ -80,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
@@ -99,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
 -------
 
@@ -124,12 +148,18 @@ 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
 and one categorical design variable.
+Other options include integer, custom, and raw (using raw variables is not
+recommended except for expert users).
 
 .. code-block:: python
 
@@ -143,7 +173,7 @@ and one categorical design variable.
    # Add a second categorical design variable with 3 levels
    my_moop.addDesign({'name': "x2", # optional, name
                       'des_type': "categorical", # required, type of variable
-                      'levels': 3 # required, number of categories
+                      'levels': ["good", "bad"] # required, category names
                      })
 
 Next, add simulations to the problem as follows using the
@@ -158,7 +188,7 @@ Next, add simulations to the problem as follows using the
 
    # Define a toy simulation for the problem, whose outputs are quadratic
    def sim_func(x):
-      if x["x2"] == 0:
+      if x["x2"] == "good":
          return np.array([(x["x1"] - 0.2) ** 2, (x["x1"] - 0.8) ** 2])
       else:
          return np.array([99.9, 99.9])
@@ -211,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!
 
@@ -222,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``
 
@@ -236,32 +275,50 @@ Our full documentation is hosted on:
 
 Please read our LICENSE_ and CONTRIBUTING_ files.
 
-Citing ParMOO:
+Citing ParMOO
+-------------
+
+Please use one of the following to cite ParMOO.
+
+Our JOSS paper:
+
+.. code-block:: bibtex
+
+    @article{parmoo-joss,
+        title       = {{ParMOO}: A {P}ython library for parallel multiobjective simulation optimization},
+        author      = {Chang, Tyler H. and Wild, Stefan M.},
+        year        = {2023},
+        journal     = {To appear in The Journal of Open Source Software}
+    }
 
- * Please use the following to cite ParMOO:
+Our online documentation:
 
 .. code-block:: bibtex
 
     @techreport{parmoo-docs,
         title       = {{ParMOO}: {P}ython library for parallel multiobjective simulation optimization},
-        author      = {Chang, Tyler H. and Wild, Stefan M.},
+        author      = {Chang, Tyler H. and Wild, Stefan M. and Dickinson, Hyrum},
         institution = {Argonne National Laboratory},
-        number      = {Version 0.1.0},
-        year        = {2022},
+        number      = {Version 0.2.0},
+        year        = {2023},
         url         = {https://parmoo.readthedocs.io/en/latest}
     }
 
-
 .. _Actions: https://github.com/parmoo/parmoo/actions
 .. _CONTRIBUTING: https://github.com/parmoo/parmoo/blob/main/CONTRIBUTING.rst
+.. _dash: https://dash.plotly.com
 .. _flake8: https://flake8.pycqa.org/en/latest
 .. _GitHub: https://github.com/parmoo/parmoo
+.. _kaleido: https://github.com/plotly/Kaleido
 .. _libEnsemble: https://github.com/Libensemble/libensemble
 .. _LICENSE: https://github.com/parmoo/parmoo/blob/main/LICENSE
 .. _numpy: https://numpy.org
+.. _pandas: https://pandas.pydata.org
+.. _plotly: https://plotly.com/python
 .. _pyDOE: https://pythonhosted.org/pyDOE
 .. _pytest: https://docs.pytest.org/en/7.0.x
 .. _pytest-cov: https://pytest-cov.readthedocs.io/en/latest
 .. _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/REQUIREMENTS b/REQUIREMENTS
index fee4d09..8426f54 100644
--- a/REQUIREMENTS
+++ b/REQUIREMENTS
@@ -2,3 +2,7 @@ numpy>=1.8.2
 scipy>=1.0.0
 pyDOE>=0.3.8
 libensemble>=0.9.0
+pandas>=0.3.0
+plotly>=5.9.0
+dash>=2.5.1
+kaleido>=0.2.1
diff --git a/SUPPORT.rst b/SUPPORT.rst
new file mode 100644
index 0000000..659b54d
--- /dev/null
+++ b/SUPPORT.rst
@@ -0,0 +1,6 @@
+Support
+-------
+
+Contact the development team of ParMOO by emailing:
+
+* parmoo@mcs.anl.gov
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/api.rst b/docs/api.rst
index 6b23b11..03bee10 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -13,3 +13,4 @@ ParMOO API
    modules/libraries
    modules/dtlz
    modules/dev_tools
+   modules/viz
diff --git a/docs/dev-guide/release-proc.rst b/docs/dev-guide/release-proc.rst
index dce5b28..f2a2b23 100644
--- a/docs/dev-guide/release-proc.rst
+++ b/docs/dev-guide/release-proc.rst
@@ -60,3 +60,5 @@ After release
 -------------
 
 - Ensure all relevant GitHub issues are closed.
+- Check that the conda-forge package has tracked latest release
+  and update dependency list if needed.
diff --git a/docs/extras.rst b/docs/extras.rst
index da0a181..07d4a3f 100644
--- a/docs/extras.rst
+++ b/docs/extras.rst
@@ -57,6 +57,16 @@ and with Python's built-in multiprocessing module.
 
     python3 libe_basic_ex.py --comms local --nworkers N
 
+|
+
+ * Note: When running a ``libE_MOOP`` with Python multiprocessing, MacOS and
+   Windows systems default to using the ``spawn`` method.
+   When using the ``spawn`` method, one must enclose the ``libE_MOOP.solve()``
+   command inside an ``if __name__ == '__main__':`` block, as shown in the
+   example above.
+   Read more about the issue here:
+     https://libensemble.readthedocs.io/en/main/running_libE.html#local-comms
+
 The result from running the example is shown below.
 
 .. literalinclude:: ../examples/libe_basic_ex.out
diff --git a/docs/how-to-write.rst b/docs/how-to-write.rst
index dc91c62..4be8695 100644
--- a/docs/how-to-write.rst
+++ b/docs/how-to-write.rst
@@ -168,10 +168,13 @@ Adding Design Variables
 Design variables are added to your :mod:`MOOP <moop.MOOP>` object
 using the :meth:`addDesign(*args) <moop.MOOP.addDesign>` method.
 ParMOO currently supports
-two types of design variables:
+several types of design variables:
 
- * ``continuous`` and
- * ``categorical``.
+ * ``continuous`` (or ``real`` or ``cont``),
+ * ``integer`` (or ``int``),
+ * ``categorical`` (or ``cat``),
+ * ``custom``,
+ * ``raw`` -- not recommended, for advanced users only.
 
 To add a continuous variable, use the following format.
 
@@ -197,6 +200,21 @@ To add a continuous variable, use the following format.
    For this design variable, any two values that are closer than ``des_tol``
    will be treated as exactly equal.
 
+To add an integer design variable, use the following format.
+
+.. code-block:: python
+
+    # Add an integer design variable
+    moop.addDesign({'name': "MyIntVar", # optional
+                    'des_type': "integer",
+                    'lb': 0,
+                    'ub': 100})
+
+|
+
+ * The ``lb`` and ``ub`` keys must be integer-valued, and serve
+   the same purpose as with continuous design variables.
+
 To add a categorical design variable, use the following format.
 
 .. code-block:: python
@@ -208,9 +226,61 @@ To add a categorical design variable, use the following format.
 
 |
 
- * The ``levels`` key is is an integer specifying the
-   number of categories taken on by this design variable.
-   ParMOO will index these levels by :math:`0, 1, \ldots, \text{levels}-1`.
+ * The ``levels`` key is either an integer specifying the
+   number of categories taken on by this design variable
+   (ParMOO will index these levels by :math:`0, 1, \ldots, \text{levels}-1`)
+   or a list of strings specifying the name for each category
+   (ParMOO will use these names for the levels, e.g.,
+   ``["first cat", "second cat", ... ]``).
+
+Note that because a numpy ndarray cannot contain string entries, when
+operating with unnamed variables, the ``levels`` key may only contain
+the integer number of levels, and named categories cannot be used with
+unnamed design variables.
+
+To add a custom design variable, use the following format.
+
+.. code-block:: python
+
+    # Add a custom design variable
+    moop.addDesign({'name': "MyCustomVar", # optional
+                    'des_type': "custom",
+                    'embedding_size': 1,
+                    'embedder': my_embedding_func,
+                    'extracter': my_extracting_func,
+                    'dtype': "U25" # optional
+                    })
+
+|
+
+ * The ``embedding_size`` key tells ParMOO how many dimensions the embedding
+   for this variable will be.
+ * The ``embedder`` key should be a function that maps the input type
+   to to a point in the ``embedding_size``-dimensional unit hypercube.
+ * The ``extracter`` key should be a function that maps an arbitrary point
+   in the ``embedding_size``-dimensional unit hypercube back to the input
+   type (such that ``extracter(embedder(x)) = x``).
+ * Optionally, the ``dtype`` key is a Python ``str`` specifying the numpy
+   dtype of the input (defaults to ``U25``, i.e., a maximum 25-character
+   string).
+
+Note that because a numpy ndarray cannot contain string entries, when
+operating with unnamed variables, the ``dtype`` key is ignored, and the
+input must have a numeric type.
+
+To add a raw design variable, use the following format. Please note that
+raw design variables are not recommended, and one will typically need to
+write custom ``search``, ``surrogate``, ``optimizer``, and ``acquisition``
+functions/classes to accomodate a raw variable.
+This feature is only included to allow flexibility for expert users.
+
+.. code-block:: python
+
+    # Add a raw design variable
+    moop.addDesign({'name': "MyRawVar", # optional
+                    'des_type': "raw"})
+
+|
 
 Note that for every MOOP, at least one design variable is required
 before solving.
@@ -739,6 +809,26 @@ and
 To understand the format of these outputs, please revisit the section on
 :ref:`The name Key and ParMOO Output Types <naming>`.
 
+Finally, if you have installed ParMOO with its extra dependencies
+(see the :ref:`Advanced Installation <install>`),
+then you can visualize your results using any of the
+:func:`viz.scatter() <viz.plot.scatter>`,
+:func:`viz.parallel_coordinates() <viz.plot.parallel_coordinates>`, or
+:func:`viz.radar() <viz.plot.radar>` functions.
+
+.. code-block:: python
+
+    from parmoo.viz import scatter
+
+    scatter(moop)
+
+Note that these plots are interactive and will render in a Dash app
+hosted locally on your computer.
+There are known issues when using the Chrome browser.
+
+For more information, view the complete
+:mod:`viz API page <viz.plot>`.
+
 Built-in and Custom Components
 ------------------------------
 
diff --git a/docs/img/algorithm-flowchart.png b/docs/img/algorithm-flowchart.png
index 2421c28..9bfe6e4 100644
Binary files a/docs/img/algorithm-flowchart.png and b/docs/img/algorithm-flowchart.png differ
diff --git a/docs/img/moop-uml.png b/docs/img/moop-uml.png
index 504a71c..5cd47ac 100644
Binary files a/docs/img/moop-uml.png and b/docs/img/moop-uml.png differ
diff --git a/docs/img/moop-uml.svg b/docs/img/moop-uml.svg
index 98d459a..c6aff69 100644
--- a/docs/img/moop-uml.svg
+++ b/docs/img/moop-uml.svg
@@ -1,6 +1,6 @@
 <?xml version='1.0'?>
 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"><!-- Generated by graphviz version 2.36.0 (20140111.2315)
- --><!-- Title: structs Pages: 1 --><svg height='802pt' xmlns:svg='http://www.w3.org/2000/svg' viewBox='0.00 0.00 936.00 802.00' weidth='936pt' width='936pt' xmlns:xlink='http://www.w3.org/1999/xlink' xmlns='http://www.w3.org/2000/svg'><defs><style type='text/css'>
+ --><!-- Title: structs Pages: 1 --><svg height='832pt' xmlns:svg='http://www.w3.org/2000/svg' viewBox='0.00 0.00 970.00 832.00' weidth='970pt' width='970pt' xmlns:xlink='http://www.w3.org/1999/xlink' xmlns='http://www.w3.org/2000/svg'><defs><style type='text/css'>
 				/* devanagari */
 				@font-face {
 				font-family: 'Dekko';
@@ -27,109 +27,113 @@
 				}
 
 			</style><filter filterRes='100' id='shadow' x='0' y='0'><feGaussianBlur stdDeviation='2 2'/><feOffset dx='2' dy='2'/></filter><linearGradient id='aliceblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f0f8ff;stop-opacity:1'/></linearGradient><linearGradient id='antiquewhite' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#faebd7;stop-opacity:1'/></linearGradient><linearGradient id='antiquewhite1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffefdb;stop-opacity:1'/></linearGradient><linearGradient id='antiquewhite2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eedfcc;stop-opacity:1'/></linearGradient><linearGradient id='antiquewhite3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdc0b0;stop-opacity:1'/></linearGradient><linearGradient id='antiquewhite4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b8378;stop-opacity:1'/></linearGradient><linearGradient id='aquamarine' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7fffd4;stop-opacity:1'/></linearGradient><linearGradient id='aquamarine1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7fffd4;stop-opacity:1'/></linearGradient><linearGradient id='aquamarine2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#76eec6;stop-opacity:1'/></linearGradient><linearGradient id='aquamarine3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#66cdaa;stop-opacity:1'/></linearGradient><linearGradient id='aquamarine4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#458b74;stop-opacity:1'/></linearGradient><linearGradient id='azure' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f0ffff;stop-opacity:1'/></linearGradient><linearGradient id='azure1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f0ffff;stop-opacity:1'/></linearGradient><linearGradient id='azure2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e0eeee;stop-opacity:1'/></linearGradient><linearGradient id='azure3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c1cdcd;stop-opacity:1'/></linearGradient><linearGradient id='azure4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#838b8b;stop-opacity:1'/></linearGradient><linearGradient id='beige' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f5f5dc;stop-opacity:1'/></linearGradient><linearGradient id='bisque' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffe4c4;stop-opacity:1'/></linearGradient><linearGradient id='bisque1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffe4c4;stop-opacity:1'/></linearGradient><linearGradient id='bisque2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eed5b7;stop-opacity:1'/></linearGradient><linearGradient id='bisque3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdb79e;stop-opacity:1'/></linearGradient><linearGradient id='bisque4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b7d6b;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#000000;stop-opacity:1'/></linearGradient><linearGradient id='blue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#0000ff;stop-opacity:1'/></linearGradient><linearGradient id='blue1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#0000ff;stop-opacity:1'/></linearGradient><linearGradient id='blue2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#0000ee;stop-opacity:1'/></linearGradient><linearGradient id='blue3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#0000cd;stop-opacity:1'/></linearGradient><linearGradient id='blue4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00008b;stop-opacity:1'/></linearGradient><linearGradient id='blueviolet' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8a2be2;stop-opacity:1'/></linearGradient><linearGradient id='brown' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a52a2a;stop-opacity:1'/></linearGradient><linearGradient id='brown1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff4040;stop-opacity:1'/></linearGradient><linearGradient id='brown2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee3b3b;stop-opacity:1'/></linearGradient><linearGradient id='brown3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd3333;stop-opacity:1'/></linearGradient><linearGradient id='brown4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b2323;stop-opacity:1'/></linearGradient><linearGradient id='burlywood' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#deb887;stop-opacity:1'/></linearGradient><linearGradient id='burlywood1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffd39b;stop-opacity:1'/></linearGradient><linearGradient id='burlywood2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eec591;stop-opacity:1'/></linearGradient><linearGradient id='burlywood3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdaa7d;stop-opacity:1'/></linearGradient><linearGradient id='burlywood4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b7355;stop-opacity:1'/></linearGradient><linearGradient id='cadetblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#5f9ea0;stop-opacity:1'/></linearGradient><linearGradient id='cadetblue1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#98f5ff;stop-opacity:1'/></linearGradient><linearGradient id='cadetblue2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8ee5ee;stop-opacity:1'/></linearGradient><linearGradient id='cadetblue3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7ac5cd;stop-opacity:1'/></linearGradient><linearGradient id='cadetblue4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#53868b;stop-opacity:1'/></linearGradient><linearGradient id='chartreuse' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7fff00;stop-opacity:1'/></linearGradient><linearGradient id='chartreuse1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7fff00;stop-opacity:1'/></linearGradient><linearGradient id='chartreuse2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#76ee00;stop-opacity:1'/></linearGradient><linearGradient id='chartreuse3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#66cd00;stop-opacity:1'/></linearGradient><linearGradient id='chartreuse4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#458b00;stop-opacity:1'/></linearGradient><linearGradient id='chocolate' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d2691e;stop-opacity:1'/></linearGradient><linearGradient id='chocolate1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff7f24;stop-opacity:1'/></linearGradient><linearGradient id='chocolate2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee7621;stop-opacity:1'/></linearGradient><linearGradient id='chocolate3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd661d;stop-opacity:1'/></linearGradient><linearGradient id='chocolate4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b4513;stop-opacity:1'/></linearGradient><linearGradient id='coral' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff7f50;stop-opacity:1'/></linearGradient><linearGradient id='coral1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff7256;stop-opacity:1'/></linearGradient><linearGradient id='coral2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee6a50;stop-opacity:1'/></linearGradient><linearGradient id='coral3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd5b45;stop-opacity:1'/></linearGradient><linearGradient id='coral4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b3e2f;stop-opacity:1'/></linearGradient><linearGradient id='cornflowerblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#6495ed;stop-opacity:1'/></linearGradient><linearGradient id='cornsilk' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fff8dc;stop-opacity:1'/></linearGradient><linearGradient id='cornsilk1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fff8dc;stop-opacity:1'/></linearGradient><linearGradient id='cornsilk2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eee8cd;stop-opacity:1'/></linearGradient><linearGradient id='cornsilk3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdc8b1;stop-opacity:1'/></linearGradient><linearGradient id='cornsilk4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b8878;stop-opacity:1'/></linearGradient><linearGradient id='crimson' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#dc143c;stop-opacity:1'/></linearGradient><linearGradient id='cyan' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00ffff;stop-opacity:1'/></linearGradient><linearGradient id='cyan1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00ffff;stop-opacity:1'/></linearGradient><linearGradient id='cyan2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00eeee;stop-opacity:1'/></linearGradient><linearGradient id='cyan3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00cdcd;stop-opacity:1'/></linearGradient><linearGradient id='cyan4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#008b8b;stop-opacity:1'/></linearGradient><linearGradient id='darkgoldenrod' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b8860b;stop-opacity:1'/></linearGradient><linearGradient id='darkgoldenrod1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffb90f;stop-opacity:1'/></linearGradient><linearGradient id='darkgoldenrod2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eead0e;stop-opacity:1'/></linearGradient><linearGradient id='darkgoldenrod3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd950c;stop-opacity:1'/></linearGradient><linearGradient id='darkgoldenrod4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b6508;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#006400;stop-opacity:1'/></linearGradient><linearGradient id='       ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#556b2f;stop-opacity:1'/></linearGradient><linearGradient id='darkolivegreen2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#bcee68;stop-opacity:1'/></linearGradient><linearGradient id='darkolivegreen3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a2cd5a;stop-opacity:1'/></linearGradient><linearGradient id='darkolivegreen4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#6e8b3d;stop-opacity:1'/></linearGradient><linearGradient id='darkorange' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff8c00;stop-opacity:1'/></linearGradient><linearGradient id='darkorange1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff7f00;stop-opacity:1'/></linearGradient><linearGradient id='darkorange2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee7600;stop-opacity:1'/></linearGradient><linearGradient id='darkorange3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd6600;stop-opacity:1'/></linearGradient><linearGradient id='darkorange4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b4500;stop-opacity:1'/></linearGradient><linearGradient id='darkorchid' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9932cc;stop-opacity:1'/></linearGradient><linearGradient id='darkorchid1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#bf3eff;stop-opacity:1'/></linearGradient><linearGradient id='darkorchid2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b23aee;stop-opacity:1'/></linearGradient><linearGradient id='darkorchid3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9a32cd;stop-opacity:1'/></linearGradient><linearGradient id='darkorchid4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#68228b;stop-opacity:1'/></linearGradient><linearGradient id='darksalmon' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e9967a;stop-opacity:1'/></linearGradient><linearGradient id='darkseagreen' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8fbc8f;stop-opacity:1'/></linearGradient><linearGradient id='darkseagreen1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c1ffc1;stop-opacity:1'/></linearGradient><linearGradient id='darkseagreen2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b4eeb4;stop-opacity:1'/></linearGradient><linearGradient id='darkseagreen3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9bcd9b;stop-opacity:1'/></linearGradient><linearGradient id='darkseagreen4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#698b69;stop-opacity:1'/></linearGradient><linearGradient id='darkslateblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#483d8b;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#2f4f4f;stop-opacity:1'/></linearGradient><linearGradient id='darkslategray2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8deeee;stop-opacity:1'/></linearGradient><linearGradient id='darkslategray3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#79cdcd;stop-opacity:1'/></linearGradient><linearGradient id='darkslategray4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#528b8b;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#2f4f4f;stop-opacity:1'/></linearGradient><linearGradient id='darkviolet' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9400d3;stop-opacity:1'/></linearGradient><linearGradient id='deeppink' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff1493;stop-opacity:1'/></linearGradient><linearGradient id='deeppink1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff1493;stop-opacity:1'/></linearGradient><linearGradient id='deeppink2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee1289;stop-opacity:1'/></linearGradient><linearGradient id='deeppink3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd1076;stop-opacity:1'/></linearGradient><linearGradient id='deeppink4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b0a50;stop-opacity:1'/></linearGradient><linearGradient id='deepskyblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00bfff;stop-opacity:1'/></linearGradient><linearGradient id='deepskyblue1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00bfff;stop-opacity:1'/></linearGradient><linearGradient id='deepskyblue2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00b2ee;stop-opacity:1'/></linearGradient><linearGradient id='deepskyblue3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#009acd;stop-opacity:1'/></linearGradient><linearGradient id='deepskyblue4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00688b;stop-opacity:1'/></linearGradient><linearGradient id='       ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#696969;stop-opacity:1'/></linearGradient><linearGradient id='dodgerblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#1e90ff;stop-opacity:1'/></linearGradient><linearGradient id='dodgerblue1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#1e90ff;stop-opacity:1'/></linearGradient><linearGradient id='dodgerblue2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#1c86ee;stop-opacity:1'/></linearGradient><linearGradient id='dodgerblue3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#1874cd;stop-opacity:1'/></linearGradient><linearGradient id='dodgerblue4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#104e8b;stop-opacity:1'/></linearGradient><linearGradient id='firebrick' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b22222;stop-opacity:1'/></linearGradient><linearGradient id='firebrick1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff3030;stop-opacity:1'/></linearGradient><linearGradient id='firebrick2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee2c2c;stop-opacity:1'/></linearGradient><linearGradient id='firebrick3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd2626;stop-opacity:1'/></linearGradient><linearGradient id='firebrick4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b1a1a;stop-opacity:1'/></linearGradient><linearGradient id='floralwhite' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fffaf0;stop-opacity:1'/></linearGradient><linearGradient id='forestgreen' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#228b22;stop-opacity:1'/></linearGradient><linearGradient id='gainsboro' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#dcdcdc;stop-opacity:1'/></linearGradient><linearGradient id='ghostwhite' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f8f8ff;stop-opacity:1'/></linearGradient><linearGradient id='gold' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffd700;stop-opacity:1'/></linearGradient><linearGradient id='gold1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffd700;stop-opacity:1'/></linearGradient><linearGradient id='gold2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eec900;stop-opacity:1'/></linearGradient><linearGradient id='gold3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdad00;stop-opacity:1'/></linearGradient><linearGradient id='gold4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b7500;stop-opacity:1'/></linearGradient><linearGradient id='goldenrod' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#daa520;stop-opacity:1'/></linearGradient><linearGradient id='goldenrod1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffc125;stop-opacity:1'/></linearGradient><linearGradient id='goldenrod2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eeb422;stop-opacity:1'/></linearGradient><linearGradient id='goldenrod3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd9b1d;stop-opacity:1'/></linearGradient><linearGradient id='goldenrod4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b6914;stop-opacity:1'/></linearGradient><linearGradient id='gray' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c0c0c0;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#000000;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#050505;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#0a0a0a;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#0f0f0f;stop-opacity:1'/></linearGradient><linearGradient id='       ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#141414;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#1a1a1a;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#1f1f1f;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#242424;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#292929;stop-opacity:1'/></linearGradient><linearGradient id='       ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#2e2e2e;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#333333;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#383838;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#3d3d3d;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#424242;stop-opacity:1'/></linearGradient><linearGradient id='       ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#474747;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#4d4d4d;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#525252;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#575757;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#5c5c5c;stop-opacity:1'/></linearGradient><linearGradient id='       ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#616161;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#666666;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#6b6b6b;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#707070;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#757575;stop-opacity:1'/></linearGradient><linearGradient id='       ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7a7a7a;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7f7f7f;stop-opacity:1'/></linearGradient><linearGradient id='gray52' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#858585;stop-opacity:1'/></linearGradient><linearGradient id='gray53' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#878787;stop-opacity:1'/></linearGradient><linearGradient id='gray54' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8a8a8a;stop-opacity:1'/></linearGradient><linearGradient id='gray55' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8c8c8c;stop-opacity:1'/></linearGradient><linearGradient id='gray56' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8f8f8f;stop-opacity:1'/></linearGradient><linearGradient id='gray57' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#919191;stop-opacity:1'/></linearGradient><linearGradient id='gray58' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#949494;stop-opacity:1'/></linearGradient><linearGradient id='gray59' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#969696;stop-opacity:1'/></linearGradient><linearGradient id='gray60' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#999999;stop-opacity:1'/></linearGradient><linearGradient id='gray61' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9c9c9c;stop-opacity:1'/></linearGradient><linearGradient id='gray62' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9e9e9e;stop-opacity:1'/></linearGradient><linearGradient id='gray63' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a1a1a1;stop-opacity:1'/></linearGradient><linearGradient id='gray64' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a3a3a3;stop-opacity:1'/></linearGradient><linearGradient id='gray65' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a6a6a6;stop-opacity:1'/></linearGradient><linearGradient id='gray66' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a8a8a8;stop-opacity:1'/></linearGradient><linearGradient id='gray67' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ababab;stop-opacity:1'/></linearGradient><linearGradient id='gray68' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#adadad;stop-opacity:1'/></linearGradient><linearGradient id='gray69' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b0b0b0;stop-opacity:1'/></linearGradient><linearGradient id='gray70' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b3b3b3;stop-opacity:1'/></linearGradient><linearGradient id='gray71' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b5b5b5;stop-opacity:1'/></linearGradient><linearGradient id='gray72' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b8b8b8;stop-opacity:1'/></linearGradient><linearGradient id='gray73' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#bababa;stop-opacity:1'/></linearGradient><linearGradient id='gray74' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#bdbdbd;stop-opacity:1'/></linearGradient><linearGradient id='gray75' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#bfbfbf;stop-opacity:1'/></linearGradient><linearGradient id='gray76' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c2c2c2;stop-opacity:1'/></linearGradient><linearGradient id='gray77' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c4c4c4;stop-opacity:1'/></linearGradient><linearGradient id='gray78' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c7c7c7;stop-opacity:1'/></linearGradient><linearGradient id='gray79' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c9c9c9;stop-opacity:1'/></linearGradient><linearGradient id='gray80' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cccccc;stop-opacity:1'/></linearGradient><linearGradient id='gray81' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cfcfcf;stop-opacity:1'/></linearGradient><linearGradient id='gray82' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d1d1d1;stop-opacity:1'/></linearGradient><linearGradient id='gray83' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d4d4d4;stop-opacity:1'/></linearGradient><linearGradient id='gray84' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d6d6d6;stop-opacity:1'/></linearGradient><linearGradient id='gray85' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d9d9d9;stop-opacity:1'/></linearGradient><linearGradient id='gray86' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#dbdbdb;stop-opacity:1'/></linearGradient><linearGradient id='gray87' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#dedede;stop-opacity:1'/></linearGradient><linearGradient id='gray88' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e0e0e0;stop-opacity:1'/></linearGradient><linearGradient id='gray89' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e3e3e3;stop-opacity:1'/></linearGradient><linearGradient id='gray90' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e5e5e5;stop-opacity:1'/></linearGradient><linearGradient id='gray91' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e8e8e8;stop-opacity:1'/></linearGradient><linearGradient id='gray92' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ebebeb;stop-opacity:1'/></linearGradient><linearGradient id='gray93' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ededed;stop-opacity:1'/></linearGradient><linearGradient id='gray94' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f0f0f0;stop-opacity:1'/></linearGradient><linearGradient id='gray95' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f2f2f2;stop-opacity:1'/></linearGradient><linearGradient id='gray96' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f5f5f5;stop-opacity:1'/></linearGradient><linearGradient id='gray97' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f7f7f7;stop-opacity:1'/></linearGradient><linearGradient id='gray98' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fafafa;stop-opacity:1'/></linearGradient><linearGradient id='gray99' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fcfcfc;stop-opacity:1'/></linearGradient><linearGradient id='gray100' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffffff;stop-opacity:1'/></linearGradient><linearGradient id='green' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00ff00;stop-opacity:1'/></linearGradient><linearGradient id='green1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00ff00;stop-opacity:1'/></linearGradient><linearGradient id='green2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00ee00;stop-opacity:1'/></linearGradient><linearGradient id='green3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00cd00;stop-opacity:1'/></linearGradient><linearGradient id='green4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#008b00;stop-opacity:1'/></linearGradient><linearGradient id='greenyellow' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#adff2f;stop-opacity:1'/></linearGradient><linearGradient id='grey' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c0c0c0;stop-opacity:1'/></linearGradient><linearGradient id='       ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#000000;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#050505;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#0a0a0a;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#0f0f0f;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#141414;stop-opacity:1'/></linearGradient><linearGradient id='       ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#1a1a1a;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#1f1f1f;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#242424;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#292929;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#2e2e2e;stop-opacity:1'/></linearGradient><linearGradient id='       ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#333333;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#383838;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#3d3d3d;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#424242;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#474747;stop-opacity:1'/></linearGradient><linearGradient id='       ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#4d4d4d;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#525252;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#575757;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#5c5c5c;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#616161;stop-opacity:1'/></linearGradient><linearGradient id='       ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#666666;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#6b6b6b;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#707070;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#757575;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7a7a7a;stop-opacity:1'/></linearGradient><linearGradient id='       ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7f7f7f;stop-opacity:1'/></linearGradient><linearGradient id='grey52' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#858585;stop-opacity:1'/></linearGradient><linearGradient id='grey53' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#878787;stop-opacity:1'/></linearGradient><linearGradient id='grey54' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8a8a8a;stop-opacity:1'/></linearGradient><linearGradient id='grey55' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8c8c8c;stop-opacity:1'/></linearGradient><linearGradient id='grey56' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8f8f8f;stop-opacity:1'/></linearGradient><linearGradient id='grey57' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#919191;stop-opacity:1'/></linearGradient><linearGradient id='grey58' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#949494;stop-opacity:1'/></linearGradient><linearGradient id='grey59' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#969696;stop-opacity:1'/></linearGradient><linearGradient id='grey60' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#999999;stop-opacity:1'/></linearGradient><linearGradient id='grey61' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9c9c9c;stop-opacity:1'/></linearGradient><linearGradient id='grey62' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9e9e9e;stop-opacity:1'/></linearGradient><linearGradient id='grey63' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a1a1a1;stop-opacity:1'/></linearGradient><linearGradient id='grey64' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a3a3a3;stop-opacity:1'/></linearGradient><linearGradient id='grey65' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a6a6a6;stop-opacity:1'/></linearGradient><linearGradient id='grey66' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a8a8a8;stop-opacity:1'/></linearGradient><linearGradient id='grey67' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ababab;stop-opacity:1'/></linearGradient><linearGradient id='grey68' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#adadad;stop-opacity:1'/></linearGradient><linearGradient id='grey69' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b0b0b0;stop-opacity:1'/></linearGradient><linearGradient id='grey70' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b3b3b3;stop-opacity:1'/></linearGradient><linearGradient id='grey71' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b5b5b5;stop-opacity:1'/></linearGradient><linearGradient id='grey72' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b8b8b8;stop-opacity:1'/></linearGradient><linearGradient id='grey73' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#bababa;stop-opacity:1'/></linearGradient><linearGradient id='grey74' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#bdbdbd;stop-opacity:1'/></linearGradient><linearGradient id='grey75' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#bfbfbf;stop-opacity:1'/></linearGradient><linearGradient id='grey76' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c2c2c2;stop-opacity:1'/></linearGradient><linearGradient id='grey77' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c4c4c4;stop-opacity:1'/></linearGradient><linearGradient id='grey78' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c7c7c7;stop-opacity:1'/></linearGradient><linearGradient id='grey79' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c9c9c9;stop-opacity:1'/></linearGradient><linearGradient id='grey80' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cccccc;stop-opacity:1'/></linearGradient><linearGradient id='grey81' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cfcfcf;stop-opacity:1'/></linearGradient><linearGradient id='grey82' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d1d1d1;stop-opacity:1'/></linearGradient><linearGradient id='grey83' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d4d4d4;stop-opacity:1'/></linearGradient><linearGradient id='grey84' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d6d6d6;stop-opacity:1'/></linearGradient><linearGradient id='grey85' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d9d9d9;stop-opacity:1'/></linearGradient><linearGradient id='grey86' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#dbdbdb;stop-opacity:1'/></linearGradient><linearGradient id='grey87' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#dedede;stop-opacity:1'/></linearGradient><linearGradient id='grey88' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e0e0e0;stop-opacity:1'/></linearGradient><linearGradient id='grey89' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e3e3e3;stop-opacity:1'/></linearGradient><linearGradient id='grey90' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e5e5e5;stop-opacity:1'/></linearGradient><linearGradient id='grey91' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e8e8e8;stop-opacity:1'/></linearGradient><linearGradient id='grey92' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ebebeb;stop-opacity:1'/></linearGradient><linearGradient id='grey93' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ededed;stop-opacity:1'/></linearGradient><linearGradient id='grey94' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f0f0f0;stop-opacity:1'/></linearGradient><linearGradient id='grey95' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f2f2f2;stop-opacity:1'/></linearGradient><linearGradient id='grey96' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f5f5f5;stop-opacity:1'/></linearGradient><linearGradient id='grey97' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f7f7f7;stop-opacity:1'/></linearGradient><linearGradient id='grey98' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fafafa;stop-opacity:1'/></linearGradient><linearGradient id='grey99' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fcfcfc;stop-opacity:1'/></linearGradient><linearGradient id='grey100' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffffff;stop-opacity:1'/></linearGradient><linearGradient id='honeydew' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f0fff0;stop-opacity:1'/></linearGradient><linearGradient id='honeydew1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f0fff0;stop-opacity:1'/></linearGradient><linearGradient id='honeydew2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e0eee0;stop-opacity:1'/></linearGradient><linearGradient id='honeydew3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c1cdc1;stop-opacity:1'/></linearGradient><linearGradient id='honeydew4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#838b83;stop-opacity:1'/></linearGradient><linearGradient id='hotpink' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff69b4;stop-opacity:1'/></linearGradient><linearGradient id='hotpink1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff6eb4;stop-opacity:1'/></linearGradient><linearGradient id='hotpink2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee6aa7;stop-opacity:1'/></linearGradient><linearGradient id='hotpink3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd6090;stop-opacity:1'/></linearGradient><linearGradient id='hotpink4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b3a62;stop-opacity:1'/></linearGradient><linearGradient id='indianred' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd5c5c;stop-opacity:1'/></linearGradient><linearGradient id='indianred1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff6a6a;stop-opacity:1'/></linearGradient><linearGradient id='indianred2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee6363;stop-opacity:1'/></linearGradient><linearGradient id='indianred3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd5555;stop-opacity:1'/></linearGradient><linearGradient id='indianred4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b3a3a;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#4b0082;stop-opacity:1'/></linearGradient><linearGradient id='ivory1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fffff0;stop-opacity:1'/></linearGradient><linearGradient id='ivory2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eeeee0;stop-opacity:1'/></linearGradient><linearGradient id='ivory3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdcdc1;stop-opacity:1'/></linearGradient><linearGradient id='ivory4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b8b83;stop-opacity:1'/></linearGradient><linearGradient id='khaki' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f0e68c;stop-opacity:1'/></linearGradient><linearGradient id='khaki1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fff68f;stop-opacity:1'/></linearGradient><linearGradient id='khaki2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eee685;stop-opacity:1'/></linearGradient><linearGradient id='khaki3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdc673;stop-opacity:1'/></linearGradient><linearGradient id='khaki4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b864e;stop-opacity:1'/></linearGradient><linearGradient id='lavender' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e6e6fa;stop-opacity:1'/></linearGradient><linearGradient id='lavenderblush' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fff0f5;stop-opacity:1'/></linearGradient><linearGradient id='lavenderblush1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fff0f5;stop-opacity:1'/></linearGradient><linearGradient id='lavenderblush2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eee0e5;stop-opacity:1'/></linearGradient><linearGradient id='lavenderblush3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdc1c5;stop-opacity:1'/></linearGradient><linearGradient id='lavenderblush4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b8386;stop-opacity:1'/></linearGradient><linearGradient id='lawngreen' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7cfc00;stop-opacity:1'/></linearGradient><linearGradient id='lemonchiffon' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fffacd;stop-opacity:1'/></linearGradient><linearGradient id='lemonchiffon1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fffacd;stop-opacity:1'/></linearGradient><linearGradient id='lemonchiffon2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eee9bf;stop-opacity:1'/></linearGradient><linearGradient id='lemonchiffon3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdc9a5;stop-opacity:1'/></linearGradient><linearGradient id='lemonchiffon4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b8970;stop-opacity:1'/></linearGradient><linearGradient id='lightblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#add8e6;stop-opacity:1'/></linearGradient><linearGradient id='lightblue1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#bfefff;stop-opacity:1'/></linearGradient><linearGradient id='lightblue2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b2dfee;stop-opacity:1'/></linearGradient><linearGradient id='lightblue3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9ac0cd;stop-opacity:1'/></linearGradient><linearGradient id='lightblue4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#68838b;stop-opacity:1'/></linearGradient><linearGradient id='lightcoral' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f08080;stop-opacity:1'/></linearGradient><linearGradient id='lightcyan' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e0ffff;stop-opacity:1'/></linearGradient><linearGradient id='lightcyan1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e0ffff;stop-opacity:1'/></linearGradient><linearGradient id='lightcyan2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d1eeee;stop-opacity:1'/></linearGradient><linearGradient id='lightcyan3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b4cdcd;stop-opacity:1'/></linearGradient><linearGradient id='lightcyan4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7a8b8b;stop-opacity:1'/></linearGradient><linearGradient id='lightgoldenrod' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eedd82;stop-opacity:1'/></linearGradient><linearGradient id='lightgoldenrod1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffec8b;stop-opacity:1'/></linearGradient><linearGradient id='lightgoldenrod2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eedc82;stop-opacity:1'/></linearGradient><linearGradient id='lightgoldenrod3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdbe70;stop-opacity:1'/></linearGradient><linearGradient id='lightgoldenrod4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b814c;stop-opacity:1'/></linearGradient><linearGradient id='lightgoldenrodyellow' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fafad2;stop-opacity:1'/></linearGradient><linearGradient id='lightgray' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d3d3d3;stop-opacity:1'/></linearGradient><linearGradient id='lightpink' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffb6c1;stop-opacity:1'/></linearGradient><linearGradient id='lightpink1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffaeb9;stop-opacity:1'/></linearGradient><linearGradient id='lightpink2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eea2ad;stop-opacity:1'/></linearGradient><linearGradient id='lightpink3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd8c95;stop-opacity:1'/></linearGradient><linearGradient id='lightpink4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b5f65;stop-opacity:1'/></linearGradient><linearGradient id='lightsalmon' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffa07a;stop-opacity:1'/></linearGradient><linearGradient id='lightsalmon1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffa07a;stop-opacity:1'/></linearGradient><linearGradient id='lightsalmon2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee9572;stop-opacity:1'/></linearGradient><linearGradient id='lightsalmon3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd8162;stop-opacity:1'/></linearGradient><linearGradient id='lightsalmon4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b5742;stop-opacity:1'/></linearGradient><linearGradient id='lightseagreen' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#20b2aa;stop-opacity:1'/></linearGradient><linearGradient id='lightskyblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#87cefa;stop-opacity:1'/></linearGradient><linearGradient id='lightskyblue1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b0e2ff;stop-opacity:1'/></linearGradient><linearGradient id='lightskyblue2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a4d3ee;stop-opacity:1'/></linearGradient><linearGradient id='lightskyblue3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8db6cd;stop-opacity:1'/></linearGradient><linearGradient id='lightskyblue4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#607b8b;stop-opacity:1'/></linearGradient><linearGradient id='lightslateblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8470ff;stop-opacity:1'/></linearGradient><linearGradient id='lightslategray' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#778899;stop-opacity:1'/></linearGradient><linearGradient id='lightslategrey' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#778899;stop-opacity:1'/></linearGradient><linearGradient id='lightsteelblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b0c4de;stop-opacity:1'/></linearGradient><linearGradient id='lightsteelblue1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cae1ff;stop-opacity:1'/></linearGradient><linearGradient id='lightsteelblue2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#bcd2ee;stop-opacity:1'/></linearGradient><linearGradient id='lightsteelblue3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a2b5cd;stop-opacity:1'/></linearGradient><linearGradient id='lightsteelblue4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#6e7b8b;stop-opacity:1'/></linearGradient><linearGradient id='lightyellow' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffffe0;stop-opacity:1'/></linearGradient><linearGradient id='lightyellow1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffffe0;stop-opacity:1'/></linearGradient><linearGradient id='lightyellow2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eeeed1;stop-opacity:1'/></linearGradient><linearGradient id='lightyellow3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdcdb4;stop-opacity:1'/></linearGradient><linearGradient id='lightyellow4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b8b7a;stop-opacity:1'/></linearGradient><linearGradient id='limegreen' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#32cd32;stop-opacity:1'/></linearGradient><linearGradient id='linen' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#faf0e6;stop-opacity:1'/></linearGradient><linearGradient id='magenta' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff00ff;stop-opacity:1'/></linearGradient><linearGradient id='magenta1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff00ff;stop-opacity:1'/></linearGradient><linearGradient id='magenta2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee00ee;stop-opacity:1'/></linearGradient><linearGradient id='magenta3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd00cd;stop-opacity:1'/></linearGradient><linearGradient id='magenta4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b008b;stop-opacity:1'/></linearGradient><linearGradient id='maroon' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b03060;stop-opacity:1'/></linearGradient><linearGradient id='maroon1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff34b3;stop-opacity:1'/></linearGradient><linearGradient id='maroon2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee30a7;stop-opacity:1'/></linearGradient><linearGradient id='maroon3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd2990;stop-opacity:1'/></linearGradient><linearGradient id='maroon4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b1c62;stop-opacity:1'/></linearGradient><linearGradient id='mediumaquamarine' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#66cdaa;stop-opacity:1'/></linearGradient><linearGradient id='mediumblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#0000cd;stop-opacity:1'/></linearGradient><linearGradient id='mediumorchid' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ba55d3;stop-opacity:1'/></linearGradient><linearGradient id='mediumorchid1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#e066ff;stop-opacity:1'/></linearGradient><linearGradient id='mediumorchid2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d15fee;stop-opacity:1'/></linearGradient><linearGradient id='mediumorchid3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b452cd;stop-opacity:1'/></linearGradient><linearGradient id='mediumorchid4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7a378b;stop-opacity:1'/></linearGradient><linearGradient id='mediumpurple' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9370db;stop-opacity:1'/></linearGradient><linearGradient id='mediumpurple1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ab82ff;stop-opacity:1'/></linearGradient><linearGradient id='mediumpurple2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9f79ee;stop-opacity:1'/></linearGradient><linearGradient id='mediumpurple3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8968cd;stop-opacity:1'/></linearGradient><linearGradient id='mediumpurple4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#5d478b;stop-opacity:1'/></linearGradient><linearGradient id='mediumseagreen' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#3cb371;stop-opacity:1'/></linearGradient><linearGradient id='mediumslateblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7b68ee;stop-opacity:1'/></linearGradient><linearGradient id='mediumspringgreen' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00fa9a;stop-opacity:1'/></linearGradient><linearGradient id='mediumturquoise' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#48d1cc;stop-opacity:1'/></linearGradient><linearGradient id='mediumvioletred' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c71585;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#191970;stop-opacity:1'/></linearGradient><linearGradient id='mistyrose' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffe4e1;stop-opacity:1'/></linearGradient><linearGradient id='mistyrose1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffe4e1;stop-opacity:1'/></linearGradient><linearGradient id='mistyrose2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eed5d2;stop-opacity:1'/></linearGradient><linearGradient id='mistyrose3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdb7b5;stop-opacity:1'/></linearGradient><linearGradient id='mistyrose4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b7d7b;stop-opacity:1'/></linearGradient><linearGradient id='moccasin' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffe4b5;stop-opacity:1'/></linearGradient><linearGradient id='navajowhite' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffdead;stop-opacity:1'/></linearGradient><linearGradient id='navajowhite1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffdead;stop-opacity:1'/></linearGradient><linearGradient id='navajowhite2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eecfa1;stop-opacity:1'/></linearGradient><linearGradient id='navajowhite3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdb38b;stop-opacity:1'/></linearGradient><linearGradient id='navajowhite4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b795e;stop-opacity:1'/></linearGradient><linearGradient id='     ' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#000080;stop-opacity:1'/></linearGradient><linearGradient id='oldlace' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fdf5e6;stop-opacity:1'/></linearGradient><linearGradient id='olivedrab' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#6b8e23;stop-opacity:1'/></linearGradient><linearGradient id='olivedrab1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c0ff3e;stop-opacity:1'/></linearGradient><linearGradient id='olivedrab2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b3ee3a;stop-opacity:1'/></linearGradient><linearGradient id='olivedrab3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9acd32;stop-opacity:1'/></linearGradient><linearGradient id='olivedrab4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#698b22;stop-opacity:1'/></linearGradient><linearGradient id='orange' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffa500;stop-opacity:1'/></linearGradient><linearGradient id='orange1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffa500;stop-opacity:1'/></linearGradient><linearGradient id='orange2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee9a00;stop-opacity:1'/></linearGradient><linearGradient id='orange3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd8500;stop-opacity:1'/></linearGradient><linearGradient id='orange4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b5a00;stop-opacity:1'/></linearGradient><linearGradient id='orangered' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff4500;stop-opacity:1'/></linearGradient><linearGradient id='orangered1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff4500;stop-opacity:1'/></linearGradient><linearGradient id='orangered2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee4000;stop-opacity:1'/></linearGradient><linearGradient id='orangered3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd3700;stop-opacity:1'/></linearGradient><linearGradient id='orangered4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b2500;stop-opacity:1'/></linearGradient><linearGradient id='orchid' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#da70d6;stop-opacity:1'/></linearGradient><linearGradient id='orchid1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff83fa;stop-opacity:1'/></linearGradient><linearGradient id='orchid2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee7ae9;stop-opacity:1'/></linearGradient><linearGradient id='orchid3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd69c9;stop-opacity:1'/></linearGradient><linearGradient id='orchid4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b4789;stop-opacity:1'/></linearGradient><linearGradient id='palegoldenrod' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eee8aa;stop-opacity:1'/></linearGradient><linearGradient id='palegreen' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#98fb98;stop-opacity:1'/></linearGradient><linearGradient id='palegreen1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9aff9a;stop-opacity:1'/></linearGradient><linearGradient id='palegreen2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#90ee90;stop-opacity:1'/></linearGradient><linearGradient id='palegreen3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7ccd7c;stop-opacity:1'/></linearGradient><linearGradient id='palegreen4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#548b54;stop-opacity:1'/></linearGradient><linearGradient id='paleturquoise' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#afeeee;stop-opacity:1'/></linearGradient><linearGradient id='paleturquoise1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#bbffff;stop-opacity:1'/></linearGradient><linearGradient id='paleturquoise2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#aeeeee;stop-opacity:1'/></linearGradient><linearGradient id='paleturquoise3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#96cdcd;stop-opacity:1'/></linearGradient><linearGradient id='paleturquoise4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#668b8b;stop-opacity:1'/></linearGradient><linearGradient id='palevioletred' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#db7093;stop-opacity:1'/></linearGradient><linearGradient id='palevioletred1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff82ab;stop-opacity:1'/></linearGradient><linearGradient id='palevioletred2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee799f;stop-opacity:1'/></linearGradient><linearGradient id='palevioletred3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd6889;stop-opacity:1'/></linearGradient><linearGradient id='palevioletred4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b475d;stop-opacity:1'/></linearGradient><linearGradient id='papayawhip' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffefd5;stop-opacity:1'/></linearGradient><linearGradient id='peachpuff' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffdab9;stop-opacity:1'/></linearGradient><linearGradient id='peachpuff1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffdab9;stop-opacity:1'/></linearGradient><linearGradient id='peachpuff2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eecbad;stop-opacity:1'/></linearGradient><linearGradient id='peachpuff3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdaf95;stop-opacity:1'/></linearGradient><linearGradient id='peachpuff4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b7765;stop-opacity:1'/></linearGradient><linearGradient id='peru' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd853f;stop-opacity:1'/></linearGradient><linearGradient id='pink' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffc0cb;stop-opacity:1'/></linearGradient><linearGradient id='pink1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffb5c5;stop-opacity:1'/></linearGradient><linearGradient id='pink2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eea9b8;stop-opacity:1'/></linearGradient><linearGradient id='pink3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd919e;stop-opacity:1'/></linearGradient><linearGradient id='pink4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b636c;stop-opacity:1'/></linearGradient><linearGradient id='plum' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#dda0dd;stop-opacity:1'/></linearGradient><linearGradient id='plum1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffbbff;stop-opacity:1'/></linearGradient><linearGradient id='plum2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eeaeee;stop-opacity:1'/></linearGradient><linearGradient id='plum3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd96cd;stop-opacity:1'/></linearGradient><linearGradient id='plum4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b668b;stop-opacity:1'/></linearGradient><linearGradient id='powderblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b0e0e6;stop-opacity:1'/></linearGradient><linearGradient id='purple' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a020f0;stop-opacity:1'/></linearGradient><linearGradient id='purple1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9b30ff;stop-opacity:1'/></linearGradient><linearGradient id='purple2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#912cee;stop-opacity:1'/></linearGradient><linearGradient id='purple3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7d26cd;stop-opacity:1'/></linearGradient><linearGradient id='purple4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#551a8b;stop-opacity:1'/></linearGradient><linearGradient id='red' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff0000;stop-opacity:1'/></linearGradient><linearGradient id='red1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff0000;stop-opacity:1'/></linearGradient><linearGradient id='red2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee0000;stop-opacity:1'/></linearGradient><linearGradient id='red3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd0000;stop-opacity:1'/></linearGradient><linearGradient id='red4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b0000;stop-opacity:1'/></linearGradient><linearGradient id='rosybrown' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#bc8f8f;stop-opacity:1'/></linearGradient><linearGradient id='rosybrown1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffc1c1;stop-opacity:1'/></linearGradient><linearGradient id='rosybrown2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eeb4b4;stop-opacity:1'/></linearGradient><linearGradient id='rosybrown3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd9b9b;stop-opacity:1'/></linearGradient><linearGradient id='rosybrown4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b6969;stop-opacity:1'/></linearGradient><linearGradient id='royalblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#4169e1;stop-opacity:1'/></linearGradient><linearGradient id='royalblue1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#4876ff;stop-opacity:1'/></linearGradient><linearGradient id='royalblue2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#436eee;stop-opacity:1'/></linearGradient><linearGradient id='royalblue3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#3a5fcd;stop-opacity:1'/></linearGradient><linearGradient id='royalblue4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#27408b;stop-opacity:1'/></linearGradient><linearGradient id='saddlebrown' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b4513;stop-opacity:1'/></linearGradient><linearGradient id='salmon' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fa8072;stop-opacity:1'/></linearGradient><linearGradient id='salmon1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff8c69;stop-opacity:1'/></linearGradient><linearGradient id='salmon2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee8262;stop-opacity:1'/></linearGradient><linearGradient id='salmon3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd7054;stop-opacity:1'/></linearGradient><linearGradient id='salmon4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b4c39;stop-opacity:1'/></linearGradient><linearGradient id='sandybrown' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f4a460;stop-opacity:1'/></linearGradient><linearGradient id='seagreen' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#2e8b57;stop-opacity:1'/></linearGradient><linearGradient id='seagreen1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#54ff9f;stop-opacity:1'/></linearGradient><linearGradient id='seagreen2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#4eee94;stop-opacity:1'/></linearGradient><linearGradient id='seagreen3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#43cd80;stop-opacity:1'/></linearGradient><linearGradient id='seagreen4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#2e8b57;stop-opacity:1'/></linearGradient><linearGradient id='seashell' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fff5ee;stop-opacity:1'/></linearGradient><linearGradient id='seashell1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fff5ee;stop-opacity:1'/></linearGradient><linearGradient id='seashell2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eee5de;stop-opacity:1'/></linearGradient><linearGradient id='seashell3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdc5bf;stop-opacity:1'/></linearGradient><linearGradient id='seashell4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b8682;stop-opacity:1'/></linearGradient><linearGradient id='sienna' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#a0522d;stop-opacity:1'/></linearGradient><linearGradient id='sienna1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff8247;stop-opacity:1'/></linearGradient><linearGradient id='sienna2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee7942;stop-opacity:1'/></linearGradient><linearGradient id='sienna3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd6839;stop-opacity:1'/></linearGradient><linearGradient id='sienna4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b4726;stop-opacity:1'/></linearGradient><linearGradient id='skyblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#87ceeb;stop-opacity:1'/></linearGradient><linearGradient id='skyblue1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#87ceff;stop-opacity:1'/></linearGradient><linearGradient id='skyblue2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7ec0ee;stop-opacity:1'/></linearGradient><linearGradient id='skyblue3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#6ca6cd;stop-opacity:1'/></linearGradient><linearGradient id='skyblue4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#4a708b;stop-opacity:1'/></linearGradient><linearGradient id='slateblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#6a5acd;stop-opacity:1'/></linearGradient><linearGradient id='slateblue1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#836fff;stop-opacity:1'/></linearGradient><linearGradient id='slateblue2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#7a67ee;stop-opacity:1'/></linearGradient><linearGradient id='slateblue3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#6959cd;stop-opacity:1'/></linearGradient><linearGradient id='slateblue4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#473c8b;stop-opacity:1'/></linearGradient><linearGradient id='slategray' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#708090;stop-opacity:1'/></linearGradient><linearGradient id='slategray1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#c6e2ff;stop-opacity:1'/></linearGradient><linearGradient id='slategray2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#b9d3ee;stop-opacity:1'/></linearGradient><linearGradient id='slategray3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9fb6cd;stop-opacity:1'/></linearGradient><linearGradient id='slategray4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#6c7b8b;stop-opacity:1'/></linearGradient><linearGradient id='slategrey' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#708090;stop-opacity:1'/></linearGradient><linearGradient id='snow' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fffafa;stop-opacity:1'/></linearGradient><linearGradient id='snow1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fffafa;stop-opacity:1'/></linearGradient><linearGradient id='snow2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eee9e9;stop-opacity:1'/></linearGradient><linearGradient id='snow3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdc9c9;stop-opacity:1'/></linearGradient><linearGradient id='snow4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b8989;stop-opacity:1'/></linearGradient><linearGradient id='springgreen' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00ff7f;stop-opacity:1'/></linearGradient><linearGradient id='springgreen1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00ff7f;stop-opacity:1'/></linearGradient><linearGradient id='springgreen2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00ee76;stop-opacity:1'/></linearGradient><linearGradient id='springgreen3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00cd66;stop-opacity:1'/></linearGradient><linearGradient id='springgreen4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#008b45;stop-opacity:1'/></linearGradient><linearGradient id='steelblue' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#4682b4;stop-opacity:1'/></linearGradient><linearGradient id='steelblue1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#63b8ff;stop-opacity:1'/></linearGradient><linearGradient id='steelblue2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#5cacee;stop-opacity:1'/></linearGradient><linearGradient id='steelblue3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#4f94cd;stop-opacity:1'/></linearGradient><linearGradient id='steelblue4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#36648b;stop-opacity:1'/></linearGradient><linearGradient id='tan' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d2b48c;stop-opacity:1'/></linearGradient><linearGradient id='tan1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffa54f;stop-opacity:1'/></linearGradient><linearGradient id='tan2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee9a49;stop-opacity:1'/></linearGradient><linearGradient id='tan3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd853f;stop-opacity:1'/></linearGradient><linearGradient id='tan4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b5a2b;stop-opacity:1'/></linearGradient><linearGradient id='thistle' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d8bfd8;stop-opacity:1'/></linearGradient><linearGradient id='thistle1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffe1ff;stop-opacity:1'/></linearGradient><linearGradient id='thistle2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eed2ee;stop-opacity:1'/></linearGradient><linearGradient id='thistle3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdb5cd;stop-opacity:1'/></linearGradient><linearGradient id='thistle4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b7b8b;stop-opacity:1'/></linearGradient><linearGradient id='tomato' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff6347;stop-opacity:1'/></linearGradient><linearGradient id='tomato1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff6347;stop-opacity:1'/></linearGradient><linearGradient id='tomato2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee5c42;stop-opacity:1'/></linearGradient><linearGradient id='tomato3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd4f39;stop-opacity:1'/></linearGradient><linearGradient id='tomato4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b3626;stop-opacity:1'/></linearGradient><linearGradient id='transparent' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#fffffe;stop-opacity:1'/></linearGradient><linearGradient id='turquoise' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#40e0d0;stop-opacity:1'/></linearGradient><linearGradient id='turquoise1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00f5ff;stop-opacity:1'/></linearGradient><linearGradient id='turquoise2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00e5ee;stop-opacity:1'/></linearGradient><linearGradient id='turquoise3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00c5cd;stop-opacity:1'/></linearGradient><linearGradient id='turquoise4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#00868b;stop-opacity:1'/></linearGradient><linearGradient id='violet' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee82ee;stop-opacity:1'/></linearGradient><linearGradient id='violetred' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#d02090;stop-opacity:1'/></linearGradient><linearGradient id='violetred1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ff3e96;stop-opacity:1'/></linearGradient><linearGradient id='violetred2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ee3a8c;stop-opacity:1'/></linearGradient><linearGradient id='violetred3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cd3278;stop-opacity:1'/></linearGradient><linearGradient id='violetred4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b2252;stop-opacity:1'/></linearGradient><linearGradient id='wheat' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f5deb3;stop-opacity:1'/></linearGradient><linearGradient id='wheat1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffe7ba;stop-opacity:1'/></linearGradient><linearGradient id='wheat2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eed8ae;stop-opacity:1'/></linearGradient><linearGradient id='wheat3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdba96;stop-opacity:1'/></linearGradient><linearGradient id='wheat4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b7e66;stop-opacity:1'/></linearGradient><linearGradient id='white' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffffff;stop-opacity:1'/></linearGradient><linearGradient id='whitesmoke' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#f5f5f5;stop-opacity:1'/></linearGradient><linearGradient id='yellow' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffff00;stop-opacity:1'/></linearGradient><linearGradient id='yellow1' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#ffff00;stop-opacity:1'/></linearGradient><linearGradient id='yellow2' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#eeee00;stop-opacity:1'/></linearGradient><linearGradient id='yellow3' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#cdcd00;stop-opacity:1'/></linearGradient><linearGradient id='yellow4' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#8b8b00;stop-opacity:1'/></linearGradient><linearGradient id='yellowgreen' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:#9acd32;stop-opacity:1'/></linearGradient><linearGradient id='lightgrey' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:rgb(220,220,220);stop-opacity:1'/></linearGradient><linearGradient id='none' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(255,255,255);stop-opacity:1'/><stop offset='100%' style='stop-color:rgb(255,255,255);stop-opacity:1'/></linearGradient><linearGradient id='black' x1='0%' x2='100%' y1='0%' y2='100%'><stop offset='0%' style='stop-color:rgb(150,150,150);stop-opacity:2'/><stop offset='100%' style='stop-color:rgb(0,0,0);stop-opacity:2'/></linearGradient></defs>
-<g class='graph' id='graph0' transform='scale(1 1) rotate(0) translate(4 768)'><title>structs</title><g class='node' id='node1'><title>MOOP</title>
-<polygon fill='white' points='288.5,-271 288.5,-763 699.5,-763 699.5,-271 288.5,-271' stroke='black' style='fill:white;stroke:black;'/>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-744.8'>&lt;&lt;class&gt;&gt;</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-729.8'>MOOP</text>
-<polyline fill='none' points='288.5,-719 699.5,-719 ' stroke='black'/>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-700.8'>Scalar Surrogate Problem Solver: SurrogateOptimizer</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-685.8'> Global Hyperparameters: dict</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-670.8'> List of Design Variables: list</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-655.8'> List of Simulations: list</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-640.8'> List of Objectives: list</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-625.8'> List of Constraints: list</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-610.8'> List of Acquisitions: list</text>
-<polyline fill='none' points='288.5,-600 699.5,-600 ' stroke='black'/>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-581.8'>addDesign(*args)</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-566.8'> addSimulation(*args)</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-551.8'> addObjective(*args)</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-536.8'> addConstraint(*args)</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-521.8'> addAcquisition(*args)</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-506.8'> getDesignType()</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-491.8'> getSimulationType()</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-476.8'> getObjectiveType()</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-461.8'> getConstraintType()</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-446.8'> save(filename="parmoo")</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-431.8'> load(filename="parmoo")</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-416.8'> setCheckpoint(checkpoint, checkpoint_data=True, filename="parmoo")</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-401.8'> iterate(k)</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-386.8'> updateAll(k, batch)</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-371.8'> check_sim_db(x, s_name)</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-356.8'> update_sim_db(x, sx, s_name)</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-341.8'> evaluateSimulation(x, s_name)</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-326.8'> solve(budget)</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-311.8'> getPF()</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-296.8'> getSimulationData()</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-281.8'> getObjectiveData()</text>
+<g class='graph' id='graph0' transform='scale(1 1) rotate(0) translate(4 798)'><title>structs</title><g class='node' id='node1'><title>MOOP</title>
+<polygon fill='lightgrey' filter='url(#shadow)' points='322.5,-301 322.5,-793 733.5,-793 733.5,-301 322.5,-301' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='lightgrey' points='322.5,-301 322.5,-793 733.5,-793 733.5,-301 322.5,-301' stroke='black' style='fill:url(#lightgrey);stroke:black;'/>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-774.8'>&lt;&lt;class&gt;&gt;</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-759.8'>MOOP</text>
+<polyline fill='none' filter='url(#shadow)' points='322.5,-749 733.5,-749 ' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polyline fill='none' points='322.5,-749 733.5,-749 ' stroke='black' style='fill:url(#none);stroke:black;'/>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-730.8'>Scalar Surrogate Problem Solver: SurrogateOptimizer</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-715.8'> Global Hyperparameters: dict</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-700.8'> List of Design Variables: list</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-685.8'> List of Simulations: list</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-670.8'> List of Objectives: list</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-655.8'> List of Constraints: list</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-640.8'> List of Acquisitions: list</text>
+<polyline fill='none' filter='url(#shadow)' points='322.5,-630 733.5,-630 ' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polyline fill='none' points='322.5,-630 733.5,-630 ' stroke='black' style='fill:url(#none);stroke:black;'/>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-611.8'>addDesign(*args)</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-596.8'> addSimulation(*args)</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-581.8'> addObjective(*args)</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-566.8'> addConstraint(*args)</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-551.8'> addAcquisition(*args)</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-536.8'> getDesignType()</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-521.8'> getSimulationType()</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-506.8'> getObjectiveType()</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-491.8'> getConstraintType()</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-476.8'> save(filename="parmoo")</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-461.8'> load(filename="parmoo")</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-446.8'> setCheckpoint(checkpoint, checkpoint_data=True, filename="parmoo")</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-431.8'> iterate(k)</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-416.8'> updateAll(k, batch)</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-401.8'> check_sim_db(x, s_name)</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-386.8'> update_sim_db(x, sx, s_name)</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-371.8'> evaluateSimulation(x, s_name)</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-356.8'> solve(budget)</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-341.8'> getPF()</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-326.8'> getSimulationData()</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-311.8'> getObjectiveData()</text>
 </g><g class='node' id='node2'><title>Design Variable</title>
-<polygon fill='white' points='0,-16 0,-164 216,-164 216,-16 0,-16' stroke='black' style='fill:white;stroke:black;'/>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='108' y='-145.8'>&lt;&lt;dict&gt;&gt;</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='108' y='-130.8'>Design Variable</text>
-<polyline fill='none' points='0,-120 216,-120 ' stroke='black'/>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='108' y='-101.8'>name: String, optional</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='108' y='-86.8'>des_type: 'continuous' or 'categorical'</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='108' y='-71.8'>lb: float (only when 'continuous')</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='108' y='-56.8'>ub: float (only when 'continuous')</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='108' y='-41.8'>tol: float (only when 'continuous')</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='108' y='-26.8'>levels: int (only when 'categorical')</text>
+<polygon fill='lightgrey' filter='url(#shadow)' points='-0.5,-1 -0.5,-209 250.5,-209 250.5,-1 -0.5,-1' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='lightgrey' points='-0.5,-1 -0.5,-209 250.5,-209 250.5,-1 -0.5,-1' stroke='black' style='fill:url(#lightgrey);stroke:black;'/>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='125' y='-190.8'>&lt;&lt;dict&gt;&gt;</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='125' y='-175.8'>Design Variable</text>
+<polyline fill='none' filter='url(#shadow)' points='-0.5,-165 250.5,-165 ' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polyline fill='none' points='-0.5,-165 250.5,-165 ' stroke='black' style='fill:url(#none);stroke:black;'/>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='125' y='-146.8'>name: String, optional</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='125' y='-131.8'>des_type: 'cont', 'int', 'cat', 'custom', or 'raw'</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='125' y='-116.8'>lb: float (only when 'cont' or 'int')</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='125' y='-101.8'>ub: float (only when 'cont' or 'int')</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='125' y='-86.8'>tol: float (only when 'cont')</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='125' y='-71.8'>levels: int or list (only when 'cat')</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='125' y='-56.8'>embedding_size: int (only when 'custom')</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='125' y='-41.8'>embedder: function (only when 'custom'</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='125' y='-26.8'>extracter: function (only when 'custom')</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='125' y='-11.8'>dtype: str (only when 'custom')</text>
 </g><g class='edge' id='edge1'><title>MOOP-&gt;Design Variable</title>
-<path d='M280.094,-280.481C244.615,-241.419 210.091,-203.405 181.391,-171.806' fill='none' stroke='black'/>
-<polygon fill='none' points='280.218,-280.618 287.213,-282.371 288.286,-289.501 281.291,-287.749 280.218,-280.618' stroke='black'/>
-<polygon fill='black' points='174.603,-164.333 184.658,-168.71 177.965,-168.034 181.327,-171.735 181.327,-171.735 181.327,-171.735 177.965,-168.034 177.995,-174.761 174.603,-164.333 174.603,-164.333' stroke='black'/>
-<text style='font-size:8px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='182.175' y='-179.144'>n</text>
+<path d='M313.937,-312.283C283.392,-278.934 253.234,-246.008 226.285,-216.584' fill='none' stroke='black'/><path d='M313.937,-312.283C283.392,-278.934 253.234,-246.008 226.285,-216.584' fill='none' filter='url(#shadow)' stroke='black' style='stroke:{@stroke}; stroke-opacity:0.1; fill:none;'/>
+<polygon fill='none' filter='url(#shadow)' points='314.263,-312.639 321.265,-314.362 322.368,-321.489 315.366,-319.766 314.263,-312.639' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='none' points='314.263,-312.639 321.265,-314.362 322.368,-321.489 315.366,-319.766 314.263,-312.639' stroke='black' style='fill:url(#none);stroke:black;'/>
+<polygon fill='black' filter='url(#shadow)' points='219.504,-209.181 229.577,-213.515 222.881,-212.868 226.258,-216.555 226.258,-216.555 226.258,-216.555 222.881,-212.868 222.94,-219.594 219.504,-209.181 219.504,-209.181' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='black' points='219.504,-209.181 229.577,-213.515 222.881,-212.868 226.258,-216.555 226.258,-216.555 226.258,-216.555 222.881,-212.868 222.94,-219.594 219.504,-209.181 219.504,-209.181' stroke='black' style='fill:url(#black);stroke:black;'/>
+<text style='font-size:8px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='227.153' y='-223.96'>n</text>
 </g><g class='node' id='node3'><title>Simulation</title>
-<polygon fill='white' points='234.5,-1 234.5,-179 407.5,-179 407.5,-1 234.5,-1' stroke='black' style='fill:white;stroke:black;'/>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='321' y='-160.8'>&lt;&lt;dict&gt;&gt;</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='321' y='-145.8'>Simulation</text>
-<polyline fill='none' points='234.5,-135 407.5,-135 ' stroke='black'/>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='321' y='-116.8'>name: String, optional</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='321' y='-101.8'>m: int</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='321' y='-86.8'>sim_func: function</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='321' y='-71.8'>search: GlobalSearch</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='321' y='-56.8'>surrogate: SurrogateFunction</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='321' y='-41.8'>des_tol: float</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='321' y='-26.8'>hyperparams: dict</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='321' y='-11.8'>sim_db: dict, optional</text>
+<polygon fill='lightgrey' filter='url(#shadow)' points='268.5,-16 268.5,-194 441.5,-194 441.5,-16 268.5,-16' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='lightgrey' points='268.5,-16 268.5,-194 441.5,-194 441.5,-16 268.5,-16' stroke='black' style='fill:url(#lightgrey);stroke:black;'/>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='355' y='-175.8'>&lt;&lt;dict&gt;&gt;</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='355' y='-160.8'>Simulation</text>
+<polyline fill='none' filter='url(#shadow)' points='268.5,-150 441.5,-150 ' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polyline fill='none' points='268.5,-150 441.5,-150 ' stroke='black' style='fill:url(#none);stroke:black;'/>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='355' y='-131.8'>name: String, optional</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='355' y='-116.8'>m: int</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='355' y='-101.8'>sim_func: function</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='355' y='-86.8'>search: GlobalSearch</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='355' y='-71.8'>surrogate: SurrogateFunction</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='355' y='-56.8'>des_tol: float</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='355' y='-41.8'>hyperparams: dict</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='355' y='-26.8'>sim_db: dict, optional</text>
 </g><g class='edge' id='edge2'><title>MOOP-&gt;Simulation</title>
-<path d='M389.594,-259.511C379.474,-234.65 369.696,-210.629 360.847,-188.891' fill='none' stroke='black'/>
-<polygon fill='none' points='389.718,-259.816 395.685,-263.865 394.242,-270.93 388.275,-266.881 389.718,-259.816' stroke='black'/>
-<polygon fill='black' points='356.982,-179.395 364.92,-186.961 358.867,-184.026 360.752,-188.658 360.752,-188.658 360.752,-188.658 358.867,-184.026 356.584,-190.354 356.982,-179.395 356.982,-179.395' stroke='black'/>
-<text style='font-size:8px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='357.732' y='-195.681'>s</text>
+<path d='M427.088,-289.344C415.186,-259.074 403.659,-229.756 393.442,-203.772' fill='none' stroke='black'/><path d='M427.088,-289.344C415.186,-259.074 403.659,-229.756 393.442,-203.772' fill='none' filter='url(#shadow)' stroke='black' style='stroke:{@stroke}; stroke-opacity:0.1; fill:none;'/>
+<polygon fill='none' filter='url(#shadow)' points='427.245,-289.744 433.163,-293.864 431.636,-300.912 425.718,-296.792 427.245,-289.744' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='none' points='427.245,-289.744 433.163,-293.864 431.636,-300.912 425.718,-296.792 427.245,-289.744' stroke='black' style='fill:url(#none);stroke:black;'/>
+<polygon fill='black' filter='url(#shadow)' points='389.739,-194.353 397.586,-202.013 391.569,-199.007 393.398,-203.66 393.398,-203.66 393.398,-203.66 391.569,-199.007 389.21,-205.306 389.739,-194.353 389.739,-194.353' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='black' points='389.739,-194.353 397.586,-202.013 391.569,-199.007 393.398,-203.66 393.398,-203.66 393.398,-203.66 391.569,-199.007 389.21,-205.306 389.739,-194.353 389.739,-194.353' stroke='black' style='fill:url(#black);stroke:black;'/>
+<text style='font-size:8px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='390.25' y='-210.647'>s</text>
 </g><g class='node' id='node4'><title>Objective</title>
-<polygon fill='white' points='425.5,-46 425.5,-134 562.5,-134 562.5,-46 425.5,-46' stroke='black' style='fill:white;stroke:black;'/>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-115.8'>&lt;&lt;dict&gt;&gt;</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-100.8'>Objective</text>
-<polyline fill='none' points='425.5,-90 562.5,-90 ' stroke='black'/>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-71.8'>name: String, optional</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='494' y='-56.8'>obj_func: function</text>
+<polygon fill='lightgrey' filter='url(#shadow)' points='459.5,-61 459.5,-149 596.5,-149 596.5,-61 459.5,-61' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='lightgrey' points='459.5,-61 459.5,-149 596.5,-149 596.5,-61 459.5,-61' stroke='black' style='fill:url(#lightgrey);stroke:black;'/>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-130.8'>&lt;&lt;dict&gt;&gt;</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-115.8'>Objective</text>
+<polyline fill='none' filter='url(#shadow)' points='459.5,-105 596.5,-105 ' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polyline fill='none' points='459.5,-105 596.5,-105 ' stroke='black' style='fill:url(#none);stroke:black;'/>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-86.8'>name: String, optional</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='528' y='-71.8'>obj_func: function</text>
 </g><g class='edge' id='edge3'><title>MOOP-&gt;Objective</title>
-<path d='M494,-258.898C494,-215.603 494,-174.895 494,-144.407' fill='none' stroke='black'/>
-<polygon fill='none' points='494,-258.93 498,-264.93 494,-270.93 490,-264.93 494,-258.93' stroke='black'/>
-<polygon fill='black' points='494,-134.062 498.5,-144.062 494,-139.062 494,-144.062 494,-144.062 494,-144.062 494,-139.062 489.5,-144.062 494,-134.062 494,-134.062' stroke='black'/>
-<text style='font-size:8px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='487.16' y='-149.156'>o</text>
+<path d='M528,-288.591C528,-239.569 528,-193.101 528,-159.31' fill='none' stroke='black'/><path d='M528,-288.591C528,-239.569 528,-193.101 528,-159.31' fill='none' filter='url(#shadow)' stroke='black' style='stroke:{@stroke}; stroke-opacity:0.1; fill:none;'/>
+<polygon fill='none' filter='url(#shadow)' points='528,-288.912 532,-294.912 528,-300.912 524,-294.912 528,-288.912' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='none' points='528,-288.912 532,-294.912 528,-300.912 524,-294.912 528,-288.912' stroke='black' style='fill:url(#none);stroke:black;'/>
+<polygon fill='black' filter='url(#shadow)' points='528,-149.241 532.5,-159.241 528,-154.241 528,-159.241 528,-159.241 528,-159.241 528,-154.241 523.5,-159.241 528,-149.241 528,-149.241' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='black' points='528,-149.241 532.5,-159.241 528,-154.241 528,-159.241 528,-159.241 528,-159.241 528,-154.241 523.5,-159.241 528,-149.241 528,-149.241' stroke='black' style='fill:url(#black);stroke:black;'/>
+<text style='font-size:8px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='521.16' y='-164.335'>o</text>
 </g><g class='node' id='node5'><title>Constraint</title>
-<polygon fill='white' points='580.5,-46 580.5,-134 717.5,-134 717.5,-46 580.5,-46' stroke='black' style='fill:white;stroke:black;'/>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='649' y='-115.8'>&lt;&lt;dict&gt;&gt;</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='649' y='-100.8'>Constraint</text>
-<polyline fill='none' points='580.5,-90 717.5,-90 ' stroke='black'/>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='649' y='-71.8'>name: String, optional</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='649' y='-56.8'>constraint: function</text>
+<polygon fill='lightgrey' filter='url(#shadow)' points='614.5,-61 614.5,-149 751.5,-149 751.5,-61 614.5,-61' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='lightgrey' points='614.5,-61 614.5,-149 751.5,-149 751.5,-61 614.5,-61' stroke='black' style='fill:url(#lightgrey);stroke:black;'/>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='683' y='-130.8'>&lt;&lt;dict&gt;&gt;</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='683' y='-115.8'>Constraint</text>
+<polyline fill='none' filter='url(#shadow)' points='614.5,-105 751.5,-105 ' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polyline fill='none' points='614.5,-105 751.5,-105 ' stroke='black' style='fill:url(#none);stroke:black;'/>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='683' y='-86.8'>name: String, optional</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='683' y='-71.8'>constraint: function</text>
 </g><g class='edge' id='edge4'><title>MOOP-&gt;Constraint</title>
-<path d='M587.543,-259.512C603.547,-215.63 618.596,-174.365 629.797,-143.653' fill='none' stroke='black'/>
-<polygon fill='none' points='587.49,-259.657 589.192,-266.664 583.378,-270.93 581.676,-263.923 587.49,-259.657' stroke='black'/>
-<polygon fill='black' points='633.295,-134.062 634.096,-144.999 631.582,-138.759 629.869,-143.457 629.869,-143.457 629.869,-143.457 631.582,-138.759 625.641,-141.915 633.295,-134.062 633.295,-134.062' stroke='black'/>
-<text style='font-size:8px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='620.429' y='-145.675'>p</text>
+<path d='M618.438,-289.273C635.863,-239.81 652.389,-192.895 664.359,-158.916' fill='none' stroke='black'/><path d='M618.438,-289.273C635.863,-239.81 652.389,-192.895 664.359,-158.916' fill='none' filter='url(#shadow)' stroke='black' style='stroke:{@stroke}; stroke-opacity:0.1; fill:none;'/>
+<polygon fill='none' filter='url(#shadow)' points='618.325,-289.594 620.104,-296.582 614.338,-300.912 612.559,-293.924 618.325,-289.594' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='none' points='618.325,-289.594 620.104,-296.582 614.338,-300.912 612.559,-293.924 618.325,-289.594' stroke='black' style='fill:url(#none);stroke:black;'/>
+<polygon fill='black' filter='url(#shadow)' points='667.767,-149.241 668.689,-160.168 666.106,-153.957 664.445,-158.673 664.445,-158.673 664.445,-158.673 666.106,-153.957 660.2,-157.178 667.767,-149.241 667.767,-149.241' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='black' points='667.767,-149.241 668.689,-160.168 666.106,-153.957 664.445,-158.673 664.445,-158.673 664.445,-158.673 666.106,-153.957 660.2,-157.178 667.767,-149.241 667.767,-149.241' stroke='black' style='fill:url(#black);stroke:black;'/>
+<text style='font-size:8px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='655.071' y='-160.994'>p</text>
 </g><g class='node' id='node6'><title>Acquisition</title>
-<polygon fill='white' points='736,-46 736,-134 928,-134 928,-46 736,-46' stroke='black' style='fill:white;stroke:black;'/>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='832' y='-115.8'>&lt;&lt;dict&gt;&gt;</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='832' y='-100.8'>Acquisition</text>
-<polyline fill='none' points='736,-90 928,-90 ' stroke='black'/>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='832' y='-71.8'>acquisition: AcquisitionFunction</text>
-<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='832' y='-56.8'>hyperparams: dict</text>
+<polygon fill='lightgrey' filter='url(#shadow)' points='770,-61 770,-149 962,-149 962,-61 770,-61' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='lightgrey' points='770,-61 770,-149 962,-149 962,-61 770,-61' stroke='black' style='fill:url(#lightgrey);stroke:black;'/>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='866' y='-130.8'>&lt;&lt;dict&gt;&gt;</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='866' y='-115.8'>Acquisition</text>
+<polyline fill='none' filter='url(#shadow)' points='770,-105 962,-105 ' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polyline fill='none' points='770,-105 962,-105 ' stroke='black' style='fill:url(#none);stroke:black;'/>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='866' y='-86.8'>acquisition: AcquisitionFunction</text>
+<text style='font-size:10px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='866' y='-71.8'>hyperparams: dict</text>
 </g><g class='edge' id='edge5'><title>MOOP-&gt;Acquisition</title>
-<path d='M696.516,-261.357C732.527,-216.077 766.423,-173.456 791.304,-142.171' fill='none' stroke='black'/>
-<polygon fill='none' points='696.372,-261.538 695.768,-268.724 688.903,-270.93 689.507,-263.744 696.372,-261.538' stroke='black'/>
-<polygon fill='black' points='797.753,-134.062 795.051,-144.69 794.641,-137.975 791.529,-141.889 791.529,-141.889 791.529,-141.889 794.641,-137.975 788.007,-139.088 797.753,-134.062 797.753,-134.062' stroke='black'/>
-<text style='font-size:8px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='780.701' y='-140.813'>q</text>
+<path d='M723.64,-291.321C762.828,-240.306 800.056,-191.844 826.608,-157.28' fill='none' stroke='black'/><path d='M723.64,-291.321C762.828,-240.306 800.056,-191.844 826.608,-157.28' fill='none' filter='url(#shadow)' stroke='black' style='stroke:{@stroke}; stroke-opacity:0.1; fill:none;'/>
+<polygon fill='none' filter='url(#shadow)' points='723.583,-291.396 723.099,-298.59 716.272,-300.912 716.755,-293.717 723.583,-291.396' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='none' points='723.583,-291.396 723.099,-298.59 716.272,-300.912 716.755,-293.717 723.583,-291.396' stroke='black' style='fill:url(#none);stroke:black;'/>
+<polygon fill='black' filter='url(#shadow)' points='832.783,-149.241 830.26,-159.913 829.737,-153.206 826.691,-157.171 826.691,-157.171 826.691,-157.171 829.737,-153.206 823.122,-154.43 832.783,-149.241 832.783,-149.241' stroke='black' style='fill: black; stroke: none; fill-opacity:0.2'/><polygon fill='black' points='832.783,-149.241 830.26,-159.913 829.737,-153.206 826.691,-157.171 826.691,-157.171 826.691,-157.171 829.737,-153.206 823.122,-154.43 832.783,-149.241 832.783,-149.241' stroke='black' style='fill:url(#black);stroke:black;'/>
+<text style='font-size:8px; font-family:Verdana;' text-anchor='middle' transform='translate(0,0)' x='815.909' y='-156.278'>q</text>
 </g></g>
-<text style='fill: lightslategray; font-size: 6pt; font-family: Verdana' x='839' y='792'>CREATED WITH YUML</text></svg>
+<text style='fill: lightslategray; font-size: 6pt; font-family: Verdana' x='873' y='822'>CREATED WITH YUML</text></svg>
diff --git a/docs/img/moop.uml b/docs/img/moop.uml
index b8ce32d..3f0523a 100644
--- a/docs/img/moop.uml
+++ b/docs/img/moop.uml
@@ -9,7 +9,7 @@
 // Give class/dict details
 [<<class>>;MOOP|Scalar Surrogate Problem Solver: SurrogateOptimizer; Global Hyperparameters: dict; List of Design Variables: list; List of Simulations: list; List of Objectives: list; List of Constraints: list; List of Acquisitions: list|addDesign(*args); addSimulation(*args); addObjective(*args); addConstraint(*args); addAcquisition(*args); getDesignType(); getSimulationType(); getObjectiveType(); getConstraintType(); save(filename="parmoo"); load(filename="parmoo"); setCheckpoint(checkpoint, checkpoint_data=True, filename="parmoo"); iterate(k); updateAll(k, batch); check_sim_db(x, s_name); update_sim_db(x, sx, s_name); evaluateSimulation(x, s_name); solve(budget); getPF(); getSimulationData(); getObjectiveData()]
 
-[<<dict>>;Design Variable|name: String, optional;des_type: 'continuous' or 'categorical';lb: float (only when 'continuous');ub: float (only when 'continuous');tol: float (only when 'continuous');levels: int (only when 'categorical')]
+[<<dict>>;Design Variable|name: String, optional;des_type: 'cont', 'int', 'cat', 'custom', or 'raw';lb: float (only when 'cont' or 'int');ub: float (only when 'cont' or 'int');tol: float (only when 'cont');levels: int or list (only when 'cat');embedding_size: int (only when 'custom');embedder: function (only when 'custom';extracter: function (only when 'custom');dtype: str (only when 'custom')]
 
 [<<dict>>;Simulation|name: String, optional;m: int;sim_func: function;search: GlobalSearch;surrogate: SurrogateFunction;des_tol: float;hyperparams: dict;sim_db: dict, optional;]
 
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 d265bcb..fa50e2c 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -10,12 +10,19 @@ installed depending on your choice of method:
  * numpy_ -- for data structures and performant numerical linear algebra
  * scipy_ -- for scientific calculations needed for specific modules
  * pyDOE_ -- for generating experimental designs
+ * pandas_ -- for exporting the resulting databases
 
 Additional dependencies are needed to use the additional features in
 ``parmoo.extras``:
 
  * libEnsemble_ -- for managing parallel simulation evaluations
 
+And for using the Pareto front visualization library in ``parmoo.viz``:
+
+ * plotly_ -- for generating interactive plots
+ * dash_ -- for hosting interactive plots in your browser
+ * kaleido_ -- for exporting static plots post-interaction
+
 If you want to run the tests (in ``parmoo.tests``), then you will also need:
 
  * pytest_,
@@ -30,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_.
@@ -39,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:
 
@@ -50,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
 --------------------------
 
@@ -102,15 +142,19 @@ 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
+.. _dash: https://dash.plotly.com
 .. _flake8: https://flake8.pycqa.org/en/latest
 .. _GitHub: https://github.com/parmoo/parmoo
+.. _kaleido: https://github.com/plotly/Kaleido
 .. _libEnsemble: https://github.com/Libensemble/libensemble
 .. _libEnsemble_documentation: https://libensemble.readthedocs.io/en/main/advanced_installation.html
 .. _numpy: https://numpy.org
+.. _pandas: https://pandas.pydata.org
+.. _plotly: https://plotly.com/python
 .. _pyDOE: https://pythonhosted.org/pyDOE
 .. _pytest: https://docs.pytest.org/en/7.0.x
 .. _pytest-cov: https://pytest-cov.readthedocs.io/en/latest
diff --git a/docs/modules/viz.rst b/docs/modules/viz.rst
new file mode 100644
index 0000000..1ca7209
--- /dev/null
+++ b/docs/modules/viz.rst
@@ -0,0 +1,90 @@
+The Interactive Visualization (viz) Library
+-------------------------------------------
+
+Easily generate a locally-hosted GUI for interactively visualizing your MOOP
+results.
+
+The public interface can be accessed by importing the viz module, which contains
+three functions for creating interactive visualizations of the approximate
+Pareto front and objective data; these visualizations run on the browser in a
+Dash app.
+*There is a known issue when using parmoo.viz in the Chrome browser,
+Firefox or Safari is recommended.*
+
+.. code-block:: python
+
+    import parmoo.viz
+
+Public Plotting Functions Running in Python Plotly and Dash
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. automodule:: viz.plot
+..    :members: viz/plot
+
+.. autofunction:: scatter
+.. autofunction:: parallel_coordinates
+.. autofunction:: radar
+
+Other Private Classes and Functions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are several other private submodules, classes, and functions contained
+in the viz module that are used for creating and hosting the dashboard.
+These do not need to be referenced by the user, but are detailed below
+for developers.
+
+Please note that documentation for some of these functions may be incomplete.
+
+.. automodule:: viz.dashboard
+..    :members: viz/dashboard
+
+.. autoclass:: Dash_App
+   :member-order: bysource
+   :members:
+
+   .. automethod:: __init__
+   .. automethod:: generate_graph
+   .. automethod:: configure
+   .. automethod:: update_height
+   .. automethod:: update_width
+   .. automethod:: update_font
+   .. automethod:: update_font_size
+   .. automethod:: update_plot_name
+   .. automethod:: update_background_color
+   .. automethod:: update_plot_type
+   .. automethod:: update_database
+   .. automethod:: evaluate_height
+   .. automethod:: evaluate_width
+   .. automethod:: evaluate_font
+   .. automethod:: evaluate_font_size
+   .. automethod:: evaluate_background_color
+   .. automethod:: evaluate_plot_name
+   .. automethod:: evaluate_plot_type
+   .. automethod:: evaluate_database
+   .. automethod:: evaluate_dataset_download
+   .. automethod:: evaluate_selected_data
+   .. automethod:: set_constraint_range
+   .. automethod:: update_constraint_range
+   .. automethod:: evaluate_selection_download
+   .. automethod:: evaluate_image_export_format
+   .. automethod:: evaluate_data_export_format
+   .. automethod:: evaluate_image_download
+   .. automethod:: evaluate_customization_options
+   .. automethod:: evaluate_export_options
+   .. automethod:: evaluate_constraint_showr
+
+.. automodule:: viz.graph
+..    :members: viz/graph
+
+.. autofunction:: generate_scatter
+.. autofunction:: generate_parallel
+.. autofunction:: generate_radar
+
+.. automodule:: viz.utilities
+..    :members: viz/utilities
+
+.. autofunction:: export_file
+.. autofunction:: set_plot_name
+.. autofunction:: set_database
+.. autofunction:: set_hover_info
+.. autofunction:: check_inputs
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index ca054a0..ecc7fc7 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -13,12 +13,12 @@ To exploit structure, ParMOO models *simulations* separately from
  * a **design variable** is an input to the problem, which we can directly
    control;
  * a **simulation** is an expensive or time-consuming process, including
-   real-world experimentation, which must be treated as a blackbox function
+   real-world experimentation, which is treated as a blackbox function
    of the design variables and evaluated sparingly;
  * an **objective** is an algebraic function of the design variables
    and/or simulation outputs, which we would like to optimize; and
  * a **constraint** is an algebraic function of the design variables
-   and/or simulation outputs, which cannot exceed some bound.
+   and/or simulation outputs, which cannot exceed a specified bound.
 
 .. figure:: img/des-sim-obj-space.png
     :alt: Designs, simulations, and objectives
@@ -26,9 +26,9 @@ To exploit structure, ParMOO models *simulations* separately from
 
 |
 
-To solve a multiobjective optimization problem (MOOP), we use surrogate models
-of the simulation outputs, together with the algebraic definition of the
-objectives and constraints.
+To solve a multiobjective optimization problem (MOOP), we use surrogate
+models of the simulation outputs, together with the algebraic definition of
+the objectives and constraints.
 
 .. only:: html
 
@@ -52,12 +52,19 @@ ParMOO's base has the following dependencies:
  * numpy_ -- for data structures and performant numerical linear algebra
  * scipy_ -- for scientific calculations needed for specific modules
  * pyDOE_ -- for generating experimental designs
+ * pandas_ -- for exporting the resulting databases
 
 Additional dependencies are needed to use the additional features in
-``parmoo.extras``.
+``parmoo.extras``:
 
  * libEnsemble_ -- for managing parallel simulation evaluations
 
+And for using the Pareto front visualization library in ``parmoo.viz``:
+
+ * plotly_ -- for generating interactive plots
+ * dash_ -- for hosting interactive plots in your browser
+ * kaleido_ -- for exporting static plots post-interaction
+
 Installation
 ------------
 
@@ -66,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
@@ -85,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
@@ -102,8 +126,8 @@ These tests are run regularly using GitHub Actions_.
 Basic Usage
 -----------
 
-ParMOO uses numpy_ in an object oriented design,
-based around the :mod:`MOOP <moop.MOOP>` class.
+ParMOO uses numpy_ in an object-oriented design, based around the
+:mod:`MOOP <moop.MOOP>` class.
 To get started, create a :mod:`MOOP <moop.MOOP>` object, using the
 :meth:`constructor <moop.MOOP.__init__>`.
 
@@ -114,14 +138,20 @@ 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 problems.
+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.
 In this example, we define one continuous and one categorical design variable.
+Other options include integer, custom, and raw (using raw variables is not
+recommended except for expert users).
 
 .. code-block:: python
 
@@ -135,7 +165,7 @@ In this example, we define one continuous and one categorical design variable.
    # Add a second categorical design variable with 3 levels
    my_moop.addDesign({'name': "x2", # optional, name
                       'des_type': "categorical", # required, type of variable
-                      'levels': 3 # required, number of categories
+                      'levels': ["good", "bad"] # required, category names
                      })
 
 Next, add simulations to the problem as follows using the
@@ -144,12 +174,13 @@ In this example, we define a toy simulation ``sim_func(x)``.
 
 .. code-block:: python
 
+   import numpy as np
    from parmoo.searches import LatinHypercube
    from parmoo.surrogates import GaussRBF
 
    # Define a toy simulation for the problem, whose outputs are quadratic
    def sim_func(x):
-      if x["x2"] == 0:
+      if x["x2"] == "good":
          return np.array([(x["x1"] - 0.2) ** 2, (x["x1"] - 0.8) ** 2])
       else:
          return np.array([99.9, 99.9])
@@ -207,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
@@ -217,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
 ----------
 
@@ -233,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``
 
@@ -250,11 +300,15 @@ Please read our LICENSE_ and CONTRIBUTING_ files.
 
 .. _Actions: https://github.com/parmoo/parmoo/actions
 .. _CONTRIBUTING: https://github.com/parmoo/parmoo/blob/main/CONTRIBUTING.rst
+.. _dash: https://dash.plotly.com
 .. _flake8: https://flake8.pycqa.org/en/latest
 .. _GitHub: https://github.com/parmoo/parmoo
+.. _kaleido: https://github.com/plotly/Kaleido
 .. _libEnsemble: https://github.com/Libensemble/libensemble
 .. _LICENSE: https://github.com/parmoo/parmoo/blob/main/LICENSE
 .. _numpy: https://numpy.org
+.. _pandas: https://pandas.pydata.org
+.. _plotly: https://plotly.com/python
 .. _pyDOE: https://pythonhosted.org/pyDOE
 .. _pytest: https://docs.pytest.org/en/7.0.x
 .. _pytest-cov: https://pytest-cov.readthedocs.io/en/latest
diff --git a/docs/refs.rst b/docs/refs.rst
index a9961d9..69b03b6 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``
 
@@ -20,16 +20,29 @@ Our full user guide is hosted here:
 Reference Format
 ----------------
 
- * Please use the following to cite ParMOO:
+Please use one of the following to cite ParMOO.
+
+Our JOSS paper:
+
+.. code-block:: bibtex
+
+    @article{parmoo-joss,
+        title       = {{ParMOO}: A {P}ython library for parallel multiobjective simulation optimization},
+        author      = {Chang, Tyler H. and Wild, Stefan M.},
+        year        = {2023},
+        journal     = {To appear in The Journal of Open Source Software}
+    }
+
+Our online documentation:
 
 .. code-block:: bibtex
 
     @techreport{parmoo-docs,
         title       = {{ParMOO}: {P}ython library for parallel multiobjective simulation optimization},
-        author      = {Chang, Tyler H. and Wild, Stefan M.},
+        author      = {Chang, Tyler H. and Wild, Stefan M. and Dickinson, Hyrum},
         institution = {Argonne National Laboratory},
-        number      = {Version 0.1.0},
-        year        = {2022},
+        number      = {Version 0.2.0},
+        year        = {2023},
         url         = {https://parmoo.readthedocs.io/en/latest}
     }
 
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/checkpointing.out b/examples/checkpointing.out
index 8507263..5328bc4 100644
--- a/examples/checkpointing.out
+++ b/examples/checkpointing.out
@@ -1,23 +1,26 @@
-[(0.79211912, 0, 3.50605056e-01, 6.21082186e-05, -0.69211912)
- (0.78284178, 0, 3.39704540e-01, 2.94404531e-04, -0.68284178)
- (0.75488481, 0, 3.07897153e-01, 2.03538033e-03, -0.65488481)
- (0.73852739, 0, 2.90011748e-01, 3.77888196e-03, -0.63852739)
- (0.72387895, 0, 2.74449155e-01, 5.79441410e-03, -0.62387895)
- (0.70898637, 0, 2.59067128e-01, 8.28348032e-03, -0.60898637)
- (0.703127  , 0, 2.53136776e-01, 9.38437854e-03, -0.603127  )
- (0.6953145 , 0, 2.45336452e-01, 1.09590544e-02, -0.5953145 )
- (0.68066606, 0, 2.31039862e-01, 1.42405891e-02, -0.58066606)
- (0.55200395, 0, 1.23906782e-01, 6.15020403e-02, -0.45200395)
- (0.52221879, 0, 1.03824952e-01, 7.71623980e-02, -0.42221879)
- (0.46631059, 0, 7.09213312e-02, 1.11348621e-01, -0.36631059)
- (0.45849809, 0, 6.68212634e-02, 1.16623553e-01, -0.35849809)
- (0.45190629, 0, 6.34567813e-02, 1.21169228e-01, -0.35190629)
- (0.45044145, 0, 6.27209204e-02, 1.22191179e-01, -0.35044145)
- (0.44848833, 0, 6.17464482e-02, 1.23560457e-01, -0.34848833)
- (0.3270645 , 0, 1.61453859e-02, 2.23667992e-01, -0.2270645 )
- (0.32315825, 0, 1.51679533e-02, 2.27378059e-01, -0.22315825)
- (0.31339262, 0, 1.28578863e-02, 2.36786742e-01, -0.21339262)
- (0.26212309, 0, 3.85927817e-03, 2.89311572e-01, -0.16212309)
- (0.23438089, 0, 1.18204540e-03, 3.19924981e-01, -0.13438089)
- (0.21753518, 0, 3.07482677e-04, 3.39265262e-01, -0.11753518)] 
+[(0.79432817, 0, 3.53225972e-01, 3.21696685e-05, -0.69432817)
+ (0.75428911, 0, 3.07236413e-01, 2.08948580e-03, -0.65428911)
+ (0.74842973, 0, 3.00775170e-01, 2.65949261e-03, -0.64842973)
+ (0.74208208, 0, 2.93852976e-01, 3.35448602e-03, -0.64208208)
+ (0.70136828, 0, 2.51370153e-01, 9.72821611e-03, -0.60136828)
+ (0.68964953, 0, 2.39756663e-01, 1.21772261e-02, -0.58964953)
+ (0.61103625, 0, 1.68950798e-01, 3.57072991e-02, -0.51103625)
+ (0.5849132 , 0, 1.48158173e-01, 4.62623305e-02, -0.4849132 )
+ (0.50538467, 0, 9.32597987e-02, 8.67981908e-02, -0.40538467)
+ (0.47806531, 0, 7.73203180e-02, 1.03641943e-01, -0.37806531)
+ (0.47025281, 0, 7.30365826e-02, 1.08733208e-01, -0.37025281)
+ (0.43328659, 0, 5.44226336e-02, 1.34478724e-01, -0.33328659)
+ (0.40936081, 0, 4.38319487e-02, 1.52598977e-01, -0.30936081)
+ (0.40643112, 0, 4.26138083e-02, 1.54896461e-01, -0.30643112)
+ (0.40154831, 0, 4.06217212e-02, 1.58763749e-01, -0.30154831)
+ (0.3756694 , 0, 3.08597394e-02, 1.80056455e-01, -0.2756694 )
+ (0.35108734, 0, 2.28273846e-02, 2.01522575e-01, -0.25108734)
+ (0.33448578, 0, 1.80864247e-02, 2.16703490e-01, -0.23448578)
+ (0.32459808, 0, 1.55246824e-02, 2.26006982e-01, -0.22459808)
+ (0.30994965, 0, 1.20889246e-02, 2.40149350e-01, -0.20994965)
+ (0.30262543, 0, 1.05319783e-02, 2.47381466e-01, -0.20262543)
+ (0.29920746, 0, 9.84211978e-03, 2.50793170e-01, -0.19920746)
+ (0.28065277, 0, 6.50486944e-03, 2.69721544e-01, -0.18065277)
+ (0.2469228 , 0, 2.20174956e-03, 3.05894384e-01, -0.1469228 )
+ (0.19333394, 0, 4.44363953e-05, 3.68043712e-01, -0.09333394)] 
  dtype=[('x1', '<f8'), ('x2', '<i4'), ('f1', '<f8'), ('f2', '<f8'), ('c1', '<f8')]
diff --git a/examples/libe_basic_ex.out b/examples/libe_basic_ex.out
index 1a53c4b..979910b 100644
--- a/examples/libe_basic_ex.out
+++ b/examples/libe_basic_ex.out
@@ -1,16 +1,18 @@
-[(0.79984616, 0, 0.35981541, 2.36682476e-08, -0.69984616)
- (0.77762936, 0, 0.33365568, 5.00445613e-04, -0.67762936)
- (0.75345944, 0, 0.30631735, 2.16602406e-03, -0.65345944)
- (0.68954002, 0, 0.23964943, 1.22014078e-02, -0.58954002)
- (0.62433543, 0, 0.18006055, 3.08580422e-02, -0.52433543)
- (0.6067573 , 0, 0.1654515 , 3.73427403e-02, -0.5067573 )
- (0.59789855, 0, 0.15832326, 4.08449964e-02, -0.49789855)
- (0.50186967, 0, 0.0911253 , 8.88816919e-02, -0.40186967)
- (0.48746538, 0, 0.08263634, 9.76778911e-02, -0.38746538)
- (0.46524858, 0, 0.07035681, 1.12058514e-01, -0.36524858)
- (0.42472124, 0, 0.05049963, 1.40834151e-01, -0.32472124)
- (0.32718049, 0, 0.01617488, 2.23558289e-01, -0.22718049)
- (0.26468049, 0, 0.00418357, 2.86566978e-01, -0.16468049)
- (0.26321565, 0, 0.00399622, 2.88137443e-01, -0.16321565)
- (0.23233186, 0, 0.00104535, 3.22247121e-01, -0.13233186)] 
+[(0.77389535, 0, 3.29355870e-01, 0.00068145, -0.67389535)
+ (0.72835551, 0, 2.79159548e-01, 0.00513293, -0.62835551)
+ (0.72371684, 0, 2.74279330e-01, 0.00581912, -0.62371684)
+ (0.64640759, 0, 1.99279736e-01, 0.02359063, -0.54640759)
+ (0.63371228, 0, 1.88106339e-01, 0.02765161, -0.53371228)
+ (0.60539196, 0, 1.64342645e-01, 0.03787229, -0.50539196)
+ (0.60465954, 0, 1.63749346e-01, 0.03815789, -0.50465954)
+ (0.59403943, 0, 1.55267069e-01, 0.04241976, -0.49403943)
+ (0.59306286, 0, 1.54498414e-01, 0.04282298, -0.49306286)
+ (0.57707165, 0, 1.42183031e-01, 0.04969705, -0.47707165)
+ (0.42602984, 0, 5.10894908e-02, 0.13985368, -0.32602984)
+ (0.41260211, 0, 4.51996574e-02, 0.15007712, -0.31260211)
+ (0.36914508, 0, 2.86100579e-02, 0.18563596, -0.26914508)
+ (0.35889117, 0, 2.52464049e-02, 0.194577  , -0.25889117)
+ (0.34643646, 0, 2.14436375e-02, 0.20571988, -0.24643646)
+ (0.27458215, 0, 5.56249689e-03, 0.27606392, -0.17458215)
+ (0.20119549, 0, 1.42919543e-06, 0.35856684, -0.10119549)] 
  dtype=[('x1', '<f8'), ('x2', '<i4'), ('f1', '<f8'), ('f2', '<f8'), ('c1', '<f8')]
diff --git a/examples/libe_basic_ex.py b/examples/libe_basic_ex.py
index 875e7c5..c5aff7b 100644
--- a/examples/libe_basic_ex.py
+++ b/examples/libe_basic_ex.py
@@ -6,16 +6,6 @@
 from parmoo.acquisitions import UniformWeights
 from parmoo.optimizers import LocalGPS
 
-# Create a libE_MOOP
-my_moop = libE_MOOP(LocalGPS)
-
-# Add 2 design variables (one continuous and one categorical)
-my_moop.addDesign({'name': "x1",
-                   'des_type': "continuous",
-                   'lb': 0.0, 'ub': 1.0})
-my_moop.addDesign({'name': "x2", 'des_type': "categorical",
-                   'levels': 3})
-
 # All functions are defined below.
 
 def sim_func(x):
@@ -33,32 +23,45 @@ def obj_f2(x, s):
 def const_c1(x, s):
     return 0.1 - x["x1"]
 
-# Add the simulation (note the budget of 20 sim evals during the search phase)
-my_moop.addSimulation({'name': "MySim",
-                       'm': 2,
-                       'sim_func': sim_func,
-                       'search': LatinHypercube,
-                       'surrogate': GaussRBF,
-                       'hyperparams': {'search_budget': 20}})
-
-# Add the objectives
-my_moop.addObjective({'name': "f1", 'obj_func': obj_f1})
-my_moop.addObjective({'name': "f2", 'obj_func': obj_f2})
-
-# Add the constraint
-my_moop.addConstraint({'name': "c1", 'constraint': const_c1})
-
-# Add 3 acquisition functions
-for i in range(3):
-   my_moop.addAcquisition({'acquisition': UniformWeights,
-                           'hyperparams': {}})
-
-# Turn on checkpointing -- creates the files parmoo.moop and parmoo.surrogate.1
-my_moop.setCheckpoint(True, checkpoint_data=False, filename="parmoo")
-
-# Use sim_max = 30 to perform just 30 simulations
-my_moop.solve(sim_max=30)
-results = my_moop.getPF()
-
-# Display the solution
-print(results, "\n dtype=" + str(results.dtype))
+# When using libEnsemble with Python MP, the "solve" command must be enclosed
+# in an "if __name__ == '__main__':" block, as shown below
+if __name__ == "__main__":
+    # Create a libE_MOOP
+    my_moop = libE_MOOP(LocalGPS)
+    
+    # Add 2 design variables (one continuous and one categorical)
+    my_moop.addDesign({'name': "x1",
+                       'des_type': "continuous",
+                       'lb': 0.0, 'ub': 1.0})
+    my_moop.addDesign({'name': "x2", 'des_type': "categorical",
+                       'levels': 3})
+    
+    # Add the simulation (note the budget of 20 sim evals during search phase)
+    my_moop.addSimulation({'name': "MySim",
+                           'm': 2,
+                           'sim_func': sim_func,
+                           'search': LatinHypercube,
+                           'surrogate': GaussRBF,
+                           'hyperparams': {'search_budget': 20}})
+    
+    # Add the objectives
+    my_moop.addObjective({'name': "f1", 'obj_func': obj_f1})
+    my_moop.addObjective({'name': "f2", 'obj_func': obj_f2})
+    
+    # Add the constraint
+    my_moop.addConstraint({'name': "c1", 'constraint': const_c1})
+    
+    # Add 3 acquisition functions
+    for i in range(3):
+       my_moop.addAcquisition({'acquisition': UniformWeights,
+                               'hyperparams': {}})
+    
+    # Turn on checkpointing -- creates files parmoo.moop & parmoo.surrogate.1
+    my_moop.setCheckpoint(True, checkpoint_data=False, filename="parmoo")
+    
+    # Use sim_max = 30 to perform just 30 simulations
+    my_moop.solve(sim_max=30)
+    results = my_moop.getPF()
+    
+    # Display the solution
+    print(results, "\n dtype=" + str(results.dtype))
diff --git a/examples/quickstart.out b/examples/quickstart.out
index be69cbc..43bbeb8 100644
--- a/examples/quickstart.out
+++ b/examples/quickstart.out
@@ -1,21 +1,20 @@
-[(0.77391519, 0, 3.29378651e-01, 0.00068042, -0.67391519)
- (0.74071207, 0, 2.92369542e-01, 0.00351506, -0.64071207)
- (0.69495041, 0, 2.44975906e-01, 0.01103542, -0.59495041)
- (0.68982345, 0, 2.39927017e-01, 0.01213887, -0.58982345)
- (0.6744426 , 0, 2.25095776e-01, 0.01576466, -0.5744426 )
- (0.661259  , 0, 2.12759867e-01, 0.01924906, -0.561259  )
- (0.50934553, 0, 9.56946597e-02, 0.08448002, -0.40934553)
- (0.48102522, 0, 7.89751754e-02, 0.10174491, -0.38102522)
- (0.44099983, 0, 5.80809160e-02, 0.12888113, -0.34099983)
- (0.43660529, 0, 5.59820654e-02, 0.13205571, -0.33660529)
- (0.43221076, 0, 5.39218386e-02, 0.13526892, -0.33221076)
- (0.42781623, 0, 5.19002356e-02, 0.13852076, -0.32781623)
- (0.40682014, 0, 4.27745696e-02, 0.1545904 , -0.30682014)
- (0.32226131, 0, 1.49478271e-02, 0.22823426, -0.22226131)
- (0.32030818, 0, 1.44740585e-02, 0.23010424, -0.22030818)
- (0.30053279, 0, 1.01068420e-02, 0.24946749, -0.20053279)
- (0.27709529, 0, 5.94368387e-03, 0.27342933, -0.17709529)
- (0.27025935, 0, 4.93637673e-03, 0.28062515, -0.17025935)
- (0.23778865, 0, 1.42798209e-03, 0.3160816 , -0.13778865)
- (0.18261287, 0, 3.02312325e-04, 0.38116687, -0.08261287)] 
- dtype=[('x1', '<f8'), ('x2', '<i4'), ('f1', '<f8'), ('f2', '<f8'), ('c1', '<f8')]
+[(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 17239dd..ae628f4 100644
--- a/examples/quickstart.py
+++ b/examples/quickstart.py
@@ -12,10 +12,10 @@
                    'des_type': "continuous",
                    'lb': 0.0, 'ub': 1.0})
 my_moop.addDesign({'name': "x2", 'des_type': "categorical",
-                   'levels': 3})
+                   'levels': ["good", "bad"]})
 
 def sim_func(x):
-   if x["x2"] == 0:
+   if x["x2"] == "good":
       return np.array([(x["x1"] - 0.2) ** 2, (x["x1"] - 0.8) ** 2])
    else:
       return np.array([99.9, 99.9])
@@ -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/__init__.py b/parmoo/__init__.py
index 45abe75..ae1ab36 100644
--- a/parmoo/__init__.py
+++ b/parmoo/__init__.py
@@ -7,7 +7,7 @@
 """
 
 from .version import __version__
-__author__ = "Tyler H. Chang and Stefan M. Wild"
-__credits__ = "Argonne National Laboratory"
+__author__ = "Tyler H. Chang, Stefan M. Wild, and Hyrum Dickinson"
+__credits__ = "Argonne National Laboratory and Lawrence Berkeley National Laboratory"
 
 from .moop import MOOP
diff --git a/parmoo/acquisitions/epsilon_constraint.py b/parmoo/acquisitions/epsilon_constraint.py
index c185613..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)
@@ -62,7 +61,7 @@ def __init__(self, o, lb, ub, hyperparams):
         self.lb = lb
         return
 
-    def setTarget(self, data, constraint_func, history):
+    def setTarget(self, data, lagrange_func, history):
         """ Randomly generate a target based on current nondominated points.
 
         Args:
@@ -73,16 +72,18 @@ def setTarget(self, data, constraint_func, history):
                  * 'f_vals' (numpy.ndarray): A 2d array containing the
                    corresponding list of objective values.
 
-            constraint_func (function): A function whose components evaluate
-                to zero if an only if no constraint is violated.
+            lagrange_func (function): A function whose components correspond
+                to constraint violation amounts.
 
             history (dict): A persistent dictionary that could be used by
                 the implementation of the AcquisitionFunction to pass data
                 between iterations; also unused by this scheme.
 
         Returns:
-            numpy.ndarray: A 1d array containing a feasible starting point
-            for the scalarized problem.
+            numpy.ndarray: A 1d array containing the 'best' feasible starting
+            point for the scalarized problem (if any previous evaluations
+            were feasible) or the point in the existing database that is
+            most nearly feasible.
 
         """
 
@@ -92,12 +93,12 @@ def setTarget(self, data, constraint_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 " +
@@ -112,13 +113,13 @@ def setTarget(self, data, constraint_func, history):
                     no_data = True
             else:
                 no_data = True
-        # Check whether constraint_func() has an appropriate signature
-        if callable(constraint_func):
-            if len(inspect.signature(constraint_func).parameters) != 1:
-                raise ValueError("constraint_func() must accept exactly one"
+        # Check whether lagrange_func() has an appropriate signature
+        if callable(lagrange_func):
+            if len(inspect.signature(lagrange_func).parameters) != 1:
+                raise ValueError("lagrange_func() must accept exactly one"
                                  + " input")
         else:
-            raise ValueError("constraint_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)),
@@ -133,15 +134,15 @@ def setTarget(self, data, constraint_func, history):
             self.weights = -np.log(1.0 - np.random.random_sample(self.o))
             self.weights = self.weights[:] / sum(self.weights[:])
             # Randomly select a feasible starting point
-            x = np.random.random_sample(self.n) * (self.ub - self.lb) + self.lb
-            count = 0
-            while np.any(constraint_func(x) > 0.0):
+            x_min = np.random.random_sample(self.n) * (self.ub - self.lb) \
+                    + self.lb
+            for count in range(1000):
                 x = np.random.random_sample(self.n) * (self.ub - self.lb) \
                     + self.lb
-                count += 1
-                if count == 1000:
-                    raise ValueError("constraint_func has no feasible points")
-            return x
+                if np.dot(self.weights, lagrange_func(x)) \
+                   < np.dot(self.weights, lagrange_func(x_min)):
+                    x_min[:] = x[:]
+            return x_min
         else:
             # Randomly select pts in the convex hull of the nondominate pts
             ipts = np.random.randint(0, pf['f_vals'].shape[0], size=self.o)
@@ -181,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):
@@ -210,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 df6b9c1..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
@@ -62,21 +61,24 @@ def __init__(self, o, lb, ub, hyperparams):
         self.weights = np.zeros(o)
         return
 
-    def setTarget(self, data, constraint_func, history):
+    def setTarget(self, data, lagrange_func, history):
         """ Randomly generate a new vector of scalarizing weights.
 
         Args:
             data (dict): A dictionary specifying the current function
                 evaluation database.
 
-            constraint_func (function): A function whose components evaluate
-                to zero if an only if no constraint is violated.
+            lagrange_func (function): A function whose components correspond
+                to constraint violation amounts.
+
 
             history (dict): Another unused argument for this function.
 
         Returns:
-            numpy.ndarray: A 1d array containing a feasible starting point
-            for the scalarized problem.
+            numpy.ndarray: A 1d array containing the 'best' feasible starting
+            point for the scalarized problem (if any previous evaluations
+            were feasible) or the point in the existing database that is
+            most nearly feasible.
 
         """
 
@@ -86,12 +88,12 @@ def setTarget(self, data, constraint_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 " +
@@ -106,13 +108,13 @@ def setTarget(self, data, constraint_func, history):
                     no_data = True
             else:
                 no_data = True
-        # Check whether constraint_func() has an appropriate signature
-        if callable(constraint_func):
-            if len(inspect.signature(constraint_func).parameters) != 1:
-                raise ValueError("constraint_func() must accept exactly one"
+        # Check whether lagrange_func() has an appropriate signature
+        if callable(lagrange_func):
+            if len(inspect.signature(lagrange_func).parameters) != 1:
+                raise ValueError("lagrange_func() must accept exactly one"
                                  + " input")
         else:
-            raise ValueError("constraint_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)),
@@ -126,20 +128,21 @@ def setTarget(self, data, constraint_func, history):
         self.weights = self.weights[:] / sum(self.weights[:])
         # If pf is empty, randomly select the starting point
         if pf['x_vals'].shape[0] == 0:
-            # Randomly select a feasible starting point
-            x = np.random.random_sample(self.n) * (self.ub - self.lb) + self.lb
-            count = 0
-            while np.any(constraint_func(x) > 0):
+            # Randomly search for a good starting point
+            x_min = np.random.random_sample(self.n) * (self.ub - self.lb) \
+                    + self.lb
+            for count in range(1000):
                 x = np.random.random_sample(self.n) * (self.ub - self.lb) \
                     + self.lb
-                count += 1
-                if count == 1000:
-                    raise ValueError("constraint_func has no feasible points")
+                if np.dot(self.weights, lagrange_func(x)) \
+                   < np.dot(self.weights, lagrange_func(x_min)):
+                    x_min[:] = x[:]
+            return x_min
         else:
             i = np.argmin(np.asarray([np.dot(self.weights, fi)
                                       for fi in pf['f_vals']]))
             x = pf['x_vals'][i, :]
-        return x
+            return x
 
     def scalarize(self, f_vals):
         """ Scalarize a vector of function values using the current weights.
@@ -158,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)
 
@@ -182,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)
 
@@ -227,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
@@ -239,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:
@@ -256,21 +257,23 @@ def __init__(self, o, lb, ub, hyperparams):
             self.weights = np.ones(self.o) / float(self.o)
         return
 
-    def setTarget(self, data, constraint_func, history):
+    def setTarget(self, data, lagrange_func, history):
         """ Randomly generate a feasible starting point.
 
         Args:
             data (dict): A dictionary specifying the current function
                 evaluation database.
 
-            constraint_func (function): A function whose components evaluate
-                to zero if an only if no constraint is violated.
+            lagrange_func (function): A function whose components correspond
+                to constraint violation amounts.
 
             history (dict): Another unused argument for this function.
 
         Returns:
-            numpy.ndarray: A 1d array containing a feasible starting point
-            for the scalarized problem.
+            numpy.ndarray: A 1d array containing the 'best' feasible starting
+            point for the scalarized problem (if any previous evaluations
+            were feasible) or the point in the existing database that is
+            most nearly feasible.
 
         """
 
@@ -280,12 +283,12 @@ def setTarget(self, data, constraint_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 " +
@@ -300,13 +303,13 @@ def setTarget(self, data, constraint_func, history):
                     no_data = True
             else:
                 no_data = True
-        # Check whether constraint_func() has an appropriate signature
-        if callable(constraint_func):
-            if len(inspect.signature(constraint_func).parameters) != 1:
-                raise ValueError("constraint_func() must accept exactly one"
+        # Check whether lagrange_func() has an appropriate signature
+        if callable(lagrange_func):
+            if len(inspect.signature(lagrange_func).parameters) != 1:
+                raise ValueError("lagrange_func() must accept exactly one"
                                  + " input")
         else:
-            raise ValueError("constraint_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)),
@@ -317,20 +320,21 @@ def setTarget(self, data, constraint_func, history):
             pf = updatePF(data, {})
         # If pf is empty, randomly select the starting point
         if pf['x_vals'].shape[0] == 0:
-            # Randomly select a feasible starting point
-            x = np.random.random_sample(self.n) * (self.ub - self.lb) + self.lb
-            count = 0
-            while np.any(constraint_func(x) > 0):
+            # Randomly search for a good starting point
+            x_min = np.random.random_sample(self.n) * (self.ub - self.lb) \
+                    + self.lb
+            for count in range(1000):
                 x = np.random.random_sample(self.n) * (self.ub - self.lb) \
                     + self.lb
-                count += 1
-                if count == 1000:
-                    raise ValueError("constraint_func has no feasible points")
+                if np.dot(self.weights, lagrange_func(x)) \
+                   < np.dot(self.weights, lagrange_func(x_min)):
+                    x_min[:] = x[:]
+            return x_min
         else:
             i = np.argmin(np.asarray([np.dot(self.weights, fi)
                                       for fi in pf['f_vals']]))
             x = pf['x_vals'][i, :]
-        return x
+            return x
 
     def scalarize(self, f_vals):
         """ Scalarize a vector of function values using the current weights.
@@ -349,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)
 
@@ -373,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 bd9dcfc..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 " +
@@ -639,6 +639,36 @@ def updateAll(self, k, batch):
 
         return self.moop.updateAll(k, batch)
 
+    def moop_sim(self, H, persis_info, sim_specs, _):
+        """ Evaluates the sim function for a collection of points given in
+        ``H['x']``.
+
+        """
+
+        batch = len(H)
+        sim_names = H['sim_name']
+        H_o = np.zeros(batch, dtype=sim_specs['out'])
+        for i in range(batch):
+            namei = sim_names[i]
+            if self.moop.use_names:
+                j = -1
+                for jj, jname in enumerate(self.moop.sim_names):
+                    if jname[0] == sim_names[i]:
+                        j = jj
+                        break
+            else:
+                j = namei
+            if self.moop.use_names:
+                xx = np.zeros(1, dtype=self.moop.des_names)[0]
+                for name in self.moop.des_names:
+                    xx[name[0]] = H[name[0]][i]
+                H_o[self.moop.sim_names[j][0]][i] = \
+                    self.moop.sim_funcs[j](xx)
+            else:
+                H_o['f'][i, :self.moop.m[j]] = \
+                    self.moop.sim_funcs[j](H['x'][i])
+        return H_o, persis_info
+
     def solve(self, sim_max=200, wt_max=3600, profile=False):
         """ Solve a MOOP using ParMOO + libEnsemble.
 
@@ -667,36 +697,6 @@ def solve(self, sim_max=200, wt_max=3600, profile=False):
             import only_persistent_gens as alloc_f
         from libensemble.tools import parse_args
 
-        def moop_sim(H, persis_info, sim_specs, _):
-            """ Evaluates the sim function for a collection of points given in
-            ``H['x']``.
-
-            """
-
-            batch = len(H)
-            sim_names = H['sim_name']
-            H_o = np.zeros(batch, dtype=sim_specs['out'])
-            for i in range(batch):
-                namei = sim_names[i]
-                if self.moop.use_names:
-                    j = -1
-                    for jj, jname in enumerate(self.moop.sim_names):
-                        if jname[0] == sim_names[i]:
-                            j = jj
-                            break
-                else:
-                    j = namei
-                if self.moop.use_names:
-                    xx = np.zeros(1, dtype=self.moop.des_names)[0]
-                    for name in self.moop.des_names:
-                        xx[name[0]] = H[name[0]][i]
-                    H_o[self.moop.sim_names[j][0]][i] = \
-                        self.moop.sim_funcs[j](xx)
-                else:
-                    H_o['f'][i, :self.moop.m[j]] = \
-                        self.moop.sim_funcs[j](H['x'][i])
-            return H_o, persis_info
-
         # Create libEnsemble dictionaries
         nworkers, is_manager, libE_specs, _ = parse_args()
         if self.moop.use_names:
@@ -713,7 +713,10 @@ def moop_sim(H, persis_info, sim_specs, _):
 
         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)
@@ -726,7 +729,7 @@ def moop_sim(H, persis_info, sim_specs, _):
             all_types = x_type.copy()
             for name in f_type:
                 all_types.append(name)
-            sim_specs = {'sim_f': moop_sim,
+            sim_specs = {'sim_f': self.moop_sim,
                          'in': [name[0] for name in x_type],
                          'out': f_type}
 
@@ -735,7 +738,7 @@ def moop_sim(H, persis_info, sim_specs, _):
                          'out': x_type,
                          'user': {}}
         else:
-            sim_specs = {'sim_f': moop_sim,
+            sim_specs = {'sim_f': self.moop_sim,
                          'in': ['x', 'sim_name'],
                          'out': [('f', float, max_m)]}
 
diff --git a/parmoo/moop.py b/parmoo/moop.py
index 0a542a7..82f4d12 100644
--- a/parmoo/moop.py
+++ b/parmoo/moop.py
@@ -11,6 +11,7 @@
 import json
 from parmoo import structs
 import inspect
+import pandas as pd
 
 
 class MOOP:
@@ -82,23 +83,26 @@ class MOOP:
      * ``MOOP.evaluateSurrogates(x)``
      * ``MOOP.resetSurrogates(center)``
      * ``MOOP.evaluateConstraints(x)``
-     * ``MOOP.evaluateLagrangian(x)``
+     * ``MOOP.evaluatePenalty(x)``
      * ``MOOP.evaluateGradients(x)``
 
     """
 
     # Slots for the MOOP class
-    __slots__ = ['n', 'm', 'm_total', 'o', 'p', 's', 'n_dat', 'lb', 'ub',
-                 'n_cat_d', 'cat_lb', 'cat_scale', 'RSVT', 'mean', 'n_cat',
-                 'n_cont', 'n_lvls', 'des_order', 'cat_names',
-                 'sim_names', 'des_names', 'obj_names', 'const_names',
-                 'lam', 'objectives', 'data', 'sim_funcs', 'sim_db',
-                 'des_tols', 'searches', 'surrogates', 'optimizer',
+    __slots__ = ['n', 'm', 'm_total', 'o', 'p', 's', 'n_dat',
+                 'cont_lb', 'cont_ub', 'int_lb', 'int_ub',
+                 'n_cat_d', 'n_custom_d', 'cat_lb', 'cat_scale', 'RSVT',
+                 'mean', 'custom_embedders', 'custom_extracters',
+                 'n_cat', 'n_cont', 'n_int', 'n_custom', 'n_raw', 'n_lvls',
+                 'des_order', 'cat_names', 'sim_names',
+                 'des_names', 'obj_names', 'const_names',
+                 'lam', 'epsilon', 'objectives', 'data', 'sim_funcs',
+                 'sim_db', 'des_tols', 'searches', 'surrogates', 'optimizer',
                  'constraints', 'hyperparams', 'acquisitions', 'history',
                  'scale', 'scaled_lb', 'scaled_ub', 'scaled_des_tols',
-                 'cat_des_tols', 'use_names', 'iteration', 'checkpoint',
-                 'checkpointfile', 'checkpoint_data', 'new_checkpoint',
-                 'new_data']
+                 'cat_des_tols', 'custom_des_tols', 'use_names', 'iteration',
+                 'checkpoint', 'checkpointfile', 'checkpoint_data',
+                 'new_checkpoint', 'new_data']
 
     def __embed__(self, x):
         """ Embed a design input as n-dimensional vector for ParMOO.
@@ -117,40 +121,56 @@ def __embed__(self, x):
         """
 
         # Unpack x into an ordered unstructured array
-        x_tmp = np.zeros(self.n_cont + self.n_cat)
+        x_tmp = np.zeros(self.n_cont + self.n_cat + self.n_int + self.n_custom
+                         + self.n_raw)
         if self.use_names:
             x_labels = []
             for d_name in self.des_names:
                 x_labels.append(x[d_name[0]])
             for i, j in enumerate(self.des_order):
-                if (j >= self.n_cont) and (len(self.cat_names[j - self.n_cont])
-                                           > 0):
-                    x_tmp[i] = float(self.cat_names[j - self.n_cont].index(
-                                                                x_labels[j]))
+                if ((j in range(self.n_cont+self.n_int,
+                                self.n_cont+self.n_int+self.n_cat))
+                    and (len(self.cat_names[j - self.n_cont - self.n_int])
+                         > 0)):
+                    x_tmp[j] = float(self.cat_names[j - self.n_cont -
+                                                    self.n_int].index(
+                                                                x_labels[i]))
+                elif (j in range(self.n_cont+self.n_int+self.n_cat,
+                                 self.n_cont+self.n_int+self.n_cat +
+                                 self.n_custom)):
+                    x_tmp[j] = i
                 else:
-                    x_tmp[i] = x_labels[j]
+                    x_tmp[j] = x_labels[i]
         else:
-            x_tmp = x[self.des_order]
+            x_tmp[self.des_order] = x[:]
         # Create the output array
         xx = np.zeros(self.n)
-        # Rescale the continuous variables
+        # Rescale the continuous and integer variables
         start = 0
         end = self.n_cont
-        xx[start:end] = ((x_tmp[start:end] - self.lb[start:end]) /
+        xx[start:end] = ((x_tmp[start:end] - self.cont_lb[:]) /
+                         self.scale[start:end] + self.scaled_lb[start:end])
+        # Pull inside bounding box, in case perturbed outside
+        xx[start:end] = np.maximum(xx[start:end], self.scaled_lb[start:end])
+        xx[start:end] = np.minimum(xx[start:end], self.scaled_ub[start:end])
+        # Rescale the continuous and integer variables
+        start = end
+        end = start + self.n_int
+        xx[start:end] = ((x_tmp[start:end] - self.int_lb[:]) /
                          self.scale[start:end] + self.scaled_lb[start:end])
         # Pull inside bounding box, in case perturbed outside
         xx[start:end] = np.maximum(xx[start:end], self.scaled_lb[start:end])
         xx[start:end] = np.minimum(xx[start:end], self.scaled_ub[start:end])
         # Embed the categorical variables
         if self.n_cat_d > 0:
+            start = end
+            end = start + self.n_cat_d
             bvec = np.zeros(sum(self.n_lvls))
             count = 0
             for i, n_lvl in enumerate(self.n_lvls):
-                bvec[count + int(x_tmp[self.n_cont + i])] = 1.0
+                bvec[count + int(x_tmp[start + i])] = 1.0
                 count += n_lvl
             bvec -= self.mean
-            start = self.n_cont
-            end = self.n_cont + self.n_cat_d
             xx[start:end] = ((np.matmul(self.RSVT, bvec) - self.cat_lb[:])
                              / self.scale[start:end]
                              + self.scaled_lb[start:end])
@@ -159,6 +179,29 @@ def __embed__(self, x):
                                        self.scaled_lb[start:end])
             xx[start:end] = np.minimum(xx[start:end],
                                        self.scaled_ub[start:end])
+        # Embed the custom variables
+        for i, embed_i in enumerate(self.custom_embedders):
+            start = end
+            end = start + self.n_custom_d[i]
+            if self.use_names:
+                if end - start > 1:
+                    xx[start:end] = embed_i(x_labels[int(x_tmp[self.n_cont +
+                                                               self.n_cat +
+                                                               self.n_int +
+                                                               i])])
+                # Special rule for self.n_custom_d = 1
+                else:
+                    xx[start] = embed_i(x_labels[int(x_tmp[self.n_cont +
+                                                           self.n_cat +
+                                                           self.n_int + i])])
+            else:
+                xx[start:end] = embed_i(x_tmp[self.n_cont + self.n_cat +
+                                              self.n_int + i])
+        # Embed the raw variables
+        start = end
+        end = start + self.n_raw
+        xx[start:end] = x_tmp[self.n_cont + self.n_cat + self.n_int +
+                              self.n_custom:]
         return xx
 
     def __extract__(self, x):
@@ -179,36 +222,81 @@ def __extract__(self, x):
         """
 
         # Create the output array
-        xx = np.zeros(self.n_cont + self.n_cat)
+        xx = np.zeros(self.n_cont + self.n_cat + self.n_int + self.n_custom
+                      + self.n_raw)
         # Descale the continuous variables
         start = 0
         end = self.n_cont
         xx[start:end] = ((x[start:end] - self.scaled_lb[start:end])
-                         * self.scale[start:end] + self.lb[start:end])
+                         * self.scale[start:end] + self.cont_lb[:])
+        # Pull inside bounding box, in case perturbed outside
+        xx[start:end] = np.maximum(xx[start:end], self.cont_lb[:])
+        xx[start:end] = np.minimum(xx[start:end], self.cont_ub[:])
+        # Descale the integer variables
+        start = end
+        end = start + self.n_int
+        xx[start:end] = ((x[start:end] - self.scaled_lb[start:end])
+                         * self.scale[start:end] + self.int_lb[:])
         # Pull inside bounding box, in case perturbed outside
-        xx[start:end] = np.maximum(xx[start:end], self.lb[start:end])
-        xx[start:end] = np.minimum(xx[start:end], self.ub[start:end])
+        xx[start:end] = np.maximum(xx[start:end], self.int_lb[:])
+        xx[start:end] = np.minimum(xx[start:end], self.int_ub[:])
+        # Bin the integer variables
+        for i in range(self.n_cont, self.n_cont + self.n_int):
+            xx[i] = int(xx[i])
         # Extract categorical variables
         if self.n_cat_d > 0:
-            start = self.n_cont
-            end = self.n_cont + self.n_cat_d
+            start = end
+            end = start + self.n_cat_d
             bvec = (np.matmul(np.transpose(self.RSVT),
-                              (x[start:end] - self.scaled_lb[start:end])
-                              * self.scale[start:end] + self.cat_lb[:])
+                              (x[start:end]
+                               - self.scaled_lb[start:end])
+                              * self.scale[start:end]
+                              + self.cat_lb[:])
                     + self.mean)
             count = 0
             for i, n_lvl in enumerate(self.n_lvls):
                 xx[start+i] = np.argmax(bvec[count:count+n_lvl])
                 count += n_lvl
+        # Extract custom variables
+        for i, ex_i in enumerate(self.custom_extracters):
+            start = end
+            end = start + self.n_custom_d[i]
+            if not self.use_names:
+                xx[self.n_cont + self.n_cat + self.n_int + i] = \
+                    ex_i(x[start:end])
+        # Extract the raw variables
+        start = end
+        end = start + self.n_raw
+        xx[self.n_cont + self.n_cat + self.n_int + self.n_custom:] = \
+            x[start:end]
         # Unshuffle xx and pack into a numpy structured array
         if self.use_names:
             out = np.zeros(1, dtype=np.dtype(self.des_names))
+            n_customs = 0
             for i, j in enumerate(self.des_order):
-                if (i >= self.n_cont) and (len(self.cat_names[i - self.n_cont])
-                                           > 0):
-                    out[self.des_names[i][0]] = (self.cat_names[i -
-                                                                self.n_cont]
+                # Unpack categorical variables when cat_names given
+                if ((j in range(self.n_cont+self.n_int,
+                                self.n_cont+self.n_int+self.n_cat))
+                    and (len(self.cat_names[j - self.n_cont - self.n_int])
+                         > 0)):
+                    out[self.des_names[i][0]] = (self.cat_names[j -
+                                                                self.n_cont -
+                                                                self.n_int]
                                                                [int(xx[j])])
+                # Unpack custom variables
+                elif (j in range(self.n_cont + self.n_int + self.n_cat,
+                                 self.n_cont + self.n_int + self.n_cat +
+                                 self.n_custom)):
+                    start = (self.n_cont + self.n_cat_d + self.n_int +
+                             sum(self.n_custom_d[:n_customs]))
+                    end = start + self.n_custom_d[n_customs]
+                    exi = self.custom_extracters[n_customs]
+                    if end - start > 1:
+                        out[self.des_names[i][0]] = exi(x[start:end])
+                    # Special rule for self.n_custom_d = 1
+                    else:
+                        out[self.des_names[i][0]] = exi(x[start])
+                    n_customs += 1  # increment counter
                 else:
                     out[self.des_names[i][0]] = xx[j]
             return out[0]
@@ -350,12 +438,18 @@ def __init__(self, opt_func, hyperparams=None):
         self.des_names = []
         self.des_order = []
         self.des_tols = []
-        self.n_cont = 0
-        self.lb = []
-        self.ub = []
+        self.cont_lb = []
+        self.cont_ub = []
+        self.int_lb = []
+        self.int_ub = []
         self.n_cat = 0
+        self.n_cont = 0
+        self.n_int = 0
+        self.n_custom = 0
+        self.n_raw = 0
         self.cat_names = []
         self.n_cat_d = 0
+        self.n_custom_d = []
         self.n_lvls = []
         # Initialize the scale
         self.scale = []
@@ -363,11 +457,15 @@ def __init__(self, opt_func, hyperparams=None):
         self.scaled_ub = []
         self.scaled_des_tols = []
         self.cat_des_tols = []
+        self.custom_des_tols = []
         self.cat_lb = []
         self.cat_scale = []
+        self.epsilon = 1.0e-8
         # Initialize the embedding transformation
         self.RSVT = []
         self.mean = []
+        self.custom_embedders = []
+        self.custom_extracters = []
         # Initialize lists for storing simulation information
         self.s = 0
         self.m = []
@@ -388,7 +486,7 @@ def __init__(self, opt_func, hyperparams=None):
         self.acquisitions = []
         # Initialize empty history dict
         self.history = {}
-        # Initialize the augmented Lagrange multiplier
+        # Initialize the penalty / Lagrange multiplier
         self.lam = 1.0
         # Reset the database
         self.n_dat = 0
@@ -408,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
 
@@ -433,23 +533,41 @@ def addDesign(self, *args):
                    unspecified.
                  * 'des_type' (str): The type for this design variable.
                    Currently supported options are:
-                    * 'continuous'
-                    * 'categorical'
-                 * 'lb' (float): When des_type is 'continuous', this specifies
-                   the lower bound for the design variable. This value must
-                   be specified, and must be strictly less than 'ub'
-                   (below) up to the tolerance (below).
-                 * 'ub' (float): When des_type is 'continuous', this specifies
-                   the upper bound for the design variable. This value
-                   must be specified, and must be strictly greater than
-                   'lb' (above) up to the tolerance (below).
-                 * 'tol' (float): When des_type is 'continuous', this specifies
-                   the tolerance, i.e., the minimum spacing along this
-                   dimension, before two design values are considered to
+                    * 'continuous' (or 'cont' or 'real')
+                    * 'categorical' (or 'cat')
+                    * 'integer' (or 'int')
+                    * 'custom'
+                    * 'raw' -- for advanced use only, not recommended
+                 * 'lb' (float): When des_type is 'continuous' or 'integer',
+                   this specifies the lower bound for the design variable.
+                   This value must be specified, and must be strictly less
+                   than 'ub' (below) up to the tolerance (below).
+                 * 'ub' (float): When des_type is 'continuous' or 'integer',
+                   this specifies the upper bound for the design variable.
+                   This value must be specified, and must be strictly greater
+                   than 'lb' (above) up to the tolerance (below).
+                 * 'des_tol' (float): When des_type is 'continuous', this
+                   specifies the tolerance, i.e., the minimum spacing along
+                   this dimension, before two design values are considered to
                    have equal values in this dimension. If not specified, the
-                   default value is 1.0e-8.
-                 * 'levels' (int): When des_type is 'categorical', this
-                   specifies the number of levels for the variable.
+                   default value is 1.0e-8 * (ub - lb).
+                 * 'levels' (int or list): When des_type is 'categorical', this
+                   specifies the number of levels for the variable (when int)
+                   or the names of each valid category (when a list).
+                 * 'embedding_size' (int): When des_type is 'custom', this
+                   specifies the dimension of the custom embedding.
+                 * 'dtype' (str): When des_type is 'custom', this contains
+                   a string specifying the numpy dtype of the custom input.
+                   Only used when operating with named variables, otherwise
+                   it must be numeric. When using named variables, defaults
+                   to 'U25'.
+                 * 'embedder': When des_type is 'custom', this is a custom
+                   embedding function, which maps the input to a point in the
+                   unit hypercube of dimension 'embedding_size'.
+                 * 'extracter': When des_type is 'custom', this is a custom
+                   extracting function, which maps a point in the unit
+                   hypercube of dimension 'embedding_size' to a legal input
+                   value of type 'dtype'.
 
         """
 
@@ -464,116 +582,29 @@ 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 continous design variable to the list
-                if arg['des_type'] == "continuous":
-                    if 'des_tol' in arg.keys():
-                        if isinstance(arg['des_tol'], float):
-                            if arg['des_tol'] > 0.0:
-                                des_tol = arg['des_tol']
-                            else:
-                                raise ValueError("args['des_tol'] must be "
-                                                 + "positive")
-                        else:
-                            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 not (isinstance(arg['lb'], float) and
-                                isinstance(arg['ub'], float)):
-                            raise TypeError("args['lb'] and args['ub'] must "
-                                            + "be float types")
-                        if arg['lb'] + des_tol > arg['ub']:
-                            raise ValueError("args['lb'] must be less than "
-                                             + "args['ub'] up to the design "
-                                             + "space tolerance")
-                    else:
-                        raise AttributeError("'lb' and 'ub' keys must be "
-                                             + "present when 'des_type' is "
-                                             + "'continuous'")
-                    if 'name' in arg.keys():
-                        if isinstance(arg['name'], str):
-                            if any([arg['name'] == dname[0]
-                                    for dname in self.des_names]):
-                                raise ValueError("arg['name'] must be unique")
-                            self.des_names.append((arg['name'], 'f8'))
+            # Append a new continuous design variable (default) to the list
+            if 'des_type' not in arg or \
+               arg['des_type'] in ["continuous", "cont", "real"]:
+                if 'des_tol' in arg:
+                    if isinstance(arg['des_tol'], float):
+                        if arg['des_tol'] > 0.0:
+                            des_tol = arg['des_tol']
                         else:
-                            raise TypeError("When present, 'name' must be a "
-                                            + "str type")
+                            raise ValueError("args['des_tol'] must be "
+                                             + "positive")
                     else:
-                        self.use_names = False
-                        name = "x" + str(self.n_cont + self.n_cat + 1)
-                        self.des_names.append((name, 'f8', ))
-                    # Keep track of design variable indices for bookkeeping
-                    for i in range(len(self.des_order)):
-                        # Add 1 to all categorical variable indices
-                        if self.des_order[i] >= self.n_cont:
-                            self.des_order[i] += 1
-                    self.des_order.append(self.n_cont)
-                    self.n_cont += 1
-                    self.des_tols.append(des_tol)
-                    self.lb.append(arg['lb'])
-                    self.ub.append(arg['ub'])
-                # Append a new categorical design variable to the list
-                elif arg['des_type'] == "categorical":
-                    if 'levels' in arg.keys():
-                        if isinstance(arg['levels'], int):
-                            if arg['levels'] < 2:
-                                raise ValueError("args['levels'] must be at "
-                                                 + "least 2")
-                        elif isinstance(arg['levels'], list):
-                            if len(arg['levels']) < 2:
-                                raise ValueError("args['levels'] must contain "
-                                                 + "at least 2 categories")
-                            if any([not isinstance(lvl, str)
-                                    for lvl in arg['levels']]):
-                                raise TypeError("args['levels'] must contain "
-                                                + "strings, if a list")
-                        else:
-                            raise TypeError("args['levels'] must be an int")
-                    else:
-                        raise AttributeError("'levels' must be present when "
-                                             + "'des_type' is 'categorical'")
-                    if 'name' in arg.keys():
-                        if not isinstance(arg['name'], str):
-                            raise TypeError("When present, 'name' must be a "
-                                            + "str type")
-                    else:
-                        self.use_names = False
-                        name = "x" + str(self.n_cont + self.n_cat + 1)
-                        self.des_names.append((name, 'i4', ))
-                    # Keep track of design variable indices for bookkeeping
-                    self.des_order.append(self.n_cont + self.n_cat)
-                    self.n_cat += 1
-                    self.des_tols.append(0.5)
-                    if isinstance(arg['levels'], int):
-                        self.n_lvls.append(arg['levels'])
-                        self.cat_names.append([])
-                        if 'name' in arg.keys():
-                            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():
-                            self.des_names.append((arg['name'], 'U25'))
-                    self.__generate_encoding__()
-                else:
-                    raise(ValueError("des_type=" + arg['des_type'] +
-                                     " is not a recognized value"))
-            else:
-                # The default des_type is continuous
-                if 'des_tol' in arg.keys():
-                    if not isinstance(arg['des_tol'], float):
                         raise TypeError("args['des_tol'] must be a float")
-                    if arg['des_tol'] > 0.0:
-                        des_tol = arg['des_tol']
-                    else:
-                        raise ValueError("args['des_tol'] must be positive")
                 else:
-                    des_tol = 1.0e-8
-                if 'lb' in arg.keys() and 'ub' in arg.keys():
+                    if 'lb' in arg and 'ub' in arg and \
+                       isinstance(arg['lb'], float) and \
+                       isinstance(arg['ub'], float):
+                        des_tol = 1.0e-8 * max(arg['ub'] - arg['lb'], 1.0e-4)
+                    else:
+                        des_tol = 1.0e-8
+                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 "
@@ -586,42 +617,248 @@ 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]):
+                            raise ValueError("arg['name'] must be unique")
                         self.des_names.append((arg['name'], 'f8'))
                     else:
                         raise TypeError("When present, 'name' must be a "
                                         + "str type")
                 else:
                     self.use_names = False
-                    name = "x" + str(self.n_cont + self.n_cat + 1)
+                    name = "x" + str(self.n_cont + self.n_cat + self.n_int
+                                     + self.n_custom + self.n_raw + 1)
                     self.des_names.append((name, 'f8', ))
                 # Keep track of design variable indices for bookkeeping
                 for i in range(len(self.des_order)):
-                    # Add 1 to all categorical variable indices
+                    # Add 1 to all integer variable indices
                     if self.des_order[i] >= self.n_cont:
                         self.des_order[i] += 1
                 self.des_order.append(self.n_cont)
                 self.n_cont += 1
                 self.des_tols.append(des_tol)
-                self.lb.append(arg['lb'])
-                self.ub.append(arg['ub'])
+                self.cont_lb.append(arg['lb'])
+                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:
+                    if isinstance(arg['levels'], int):
+                        if arg['levels'] < 2:
+                            raise ValueError("args['levels'] must be at "
+                                             + "least 2")
+                    elif isinstance(arg['levels'], list):
+                        if len(arg['levels']) < 2:
+                            raise ValueError("args['levels'] must contain "
+                                             + "at least 2 categories")
+                        if any([not isinstance(lvl, str)
+                                for lvl in arg['levels']]):
+                            raise TypeError("args['levels'] must contain "
+                                            + "strings, if a list")
+                    else:
+                        raise TypeError("args['levels'] must be a list or "
+                                        + "int")
+                else:
+                    raise AttributeError("'levels' must be present when "
+                                         + "'des_type' is 'categorical'")
+                if 'name' in arg:
+                    if not isinstance(arg['name'], str):
+                        raise TypeError("When present, 'name' must be a "
+                                        + "str type")
+                else:
+                    self.use_names = False
+                    name = "x" + str(self.n_cont + self.n_cat + self.n_int
+                                     + self.n_custom + self.n_raw + 1)
+                    self.des_names.append((name, 'i4', ))
+                # Keep track of design variable indices for bookkeeping
+                for i in range(len(self.des_order)):
+                    # Add 1 to all custom variable indices
+                    if self.des_order[i] >= (self.n_cont + self.n_int +
+                                             self.n_cat):
+                        self.des_order[i] += 1
+                self.des_order.append(self.n_cont + self.n_int
+                                      + self.n_cat)
+                self.n_cat += 1
+                self.des_tols.append(0.5)
+                if isinstance(arg['levels'], int):
+                    self.n_lvls.append(arg['levels'])
+                    self.cat_names.append([])
+                    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:
+                        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 and 'ub' in arg:
+                    if not (isinstance(arg['lb'], int) and
+                            isinstance(arg['ub'], int)):
+                        raise TypeError("args['lb'] and args['ub'] must "
+                                        + "be int types")
+                    if arg['lb'] + des_tol >= arg['ub']:
+                        raise ValueError("args['lb'] must be less than "
+                                         + "or equal to args['ub'] up to "
+                                         + "the design space tolerance")
+                else:
+                    raise AttributeError("'lb' and 'ub' keys must be "
+                                         + "present when 'des_type' is "
+                                         + "'integer'")
+                if 'name' in arg:
+                    if isinstance(arg['name'], str):
+                        if any([arg['name'] == dname[0]
+                                for dname in self.des_names]):
+                            raise ValueError("arg['name'] must be unique")
+                        self.des_names.append((arg['name'], 'f8'))
+                    else:
+                        raise TypeError("When present, 'name' must be a "
+                                        + "str type")
+                else:
+                    self.use_names = False
+                    name = "x" + str(self.n_cont + self.n_cat + self.n_int
+                                     + self.n_custom + self.n_raw + 1)
+                    self.des_names.append((name, 'i4', ))
+                # Keep track of design variable indices for bookkeeping
+                for i in range(len(self.des_order)):
+                    # Add 1 to all categorical variable indices
+                    if self.des_order[i] >= self.n_cont + self.n_int:
+                        self.des_order[i] += 1
+                self.des_order.append(self.n_cont + self.n_int)
+                self.n_int += 1
+                self.des_tols.append(des_tol)
+                self.int_lb.append(arg['lb'])
+                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:
+                    if isinstance(arg['embedding_size'], int):
+                        if arg['embedding_size'] < 1:
+                            raise ValueError("args['embedding_size'] must"
+                                             + " be at least 1")
+                    else:
+                        raise TypeError("args['embedding_size'] must be a "
+                                        + "int")
+                else:
+                    raise AttributeError("'embedding_size' must be present"
+                                         + " when 'des_type' is 'custom'")
+                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:
+                    if not isinstance(arg['name'], str):
+                        raise TypeError("When present, 'name' must be a "
+                                        + "str type")
+                    if 'dtype' in arg:
+                        self.des_names.append((arg['name'], arg['dtype']))
+                    else:
+                        self.des_names.append((arg['name'], 'U25'))
+                else:
+                    self.use_names = False
+                    name = "x" + str(self.n_cont + self.n_cat + self.n_int
+                                     + self.n_custom + self.n_raw + 1)
+                    self.des_names.append((name, 'f8', ))
+                # Load the custom embedder/extracter
+                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:
+                    if not callable(arg['extracter']):
+                        raise TypeError("'extracter' must be a "
+                                        + "callable object")
+                else:
+                    raise AttributeError("'extracter' must be present"
+                                         + " when 'des_type' is 'custom'")
+                self.custom_embedders.append(arg['embedder'])
+                self.custom_extracters.append(arg['extracter'])
+                # Keep track of design variable indices for bookkeeping
+                for i in range(len(self.des_order)):
+                    # Add 1 to all raw variable indices
+                    if self.des_order[i] >= (self.n_cont + self.n_int +
+                                             self.n_cat + self.n_custom):
+                        self.des_order[i] += 1
+                self.des_order.append(self.n_cont + self.n_int +
+                                      self.n_cat + self.n_custom)
+                self.n_custom += 1
+                self.n_custom_d.append(arg["embedding_size"])
+                self.des_tols.append(1.0e-8)
+                for i in range(arg["embedding_size"]):
+                    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:
+                    if not isinstance(arg['name'], str):
+                        raise TypeError("When present, 'name' must be a "
+                                        + "str type")
+                    self.des_names.append((arg['name'], 'f8'))
+                else:
+                    self.use_names = False
+                    name = "x" + str(self.n_cont + self.n_cat + self.n_int
+                                     + self.n_custom + self.n_raw + 1)
+                    self.des_names.append((name, 'f8', ))
+                # Keep track of design variable indices for bookkeeping
+                for i in range(len(self.des_order)):
+                    # Add 1 to all later variable indices (should be none)
+                    if self.des_order[i] >= (self.n_cont + self.n_int +
+                                             self.n_cat + self.n_custom +
+                                             self.n_raw):
+                        self.des_order[i] += 1
+                self.des_order.append(self.n_cont + self.n_int +
+                                      self.n_cat + self.n_custom + self.n_raw)
+                self.n_raw += 1
+                self.des_tols.append(1.0e-8)
+            else:
+                raise(ValueError("des_type=" + arg['des_type'] +
+                                 " is not a recognized value"))
         # Set the effective design dimension
-        self.n = self.n_cat_d + self.n_cont
+        self.n = (self.n_cat_d + self.n_cont + self.n_int +
+                  sum(self.n_custom_d) + self.n_raw)
         # Set the problem scaling
         self.scaled_lb = np.zeros(self.n)
         self.scaled_ub = np.ones(self.n)
         self.scale = np.ones(self.n)
-        for i in range(self.n_cont):
-            self.scale[i] = self.ub[i] - self.lb[i]
         self.scaled_des_tols = np.zeros(self.n)
-        self.scaled_des_tols[:self.n_cont] = (self.des_tols[:self.n_cont] /
-                                              self.scale[:self.n_cont])
-        if self.n_cat_d > 0:
-            self.scaled_des_tols[self.n_cont:self.n_cont+self.n_cat_d] = \
-                self.cat_des_tols[:]
-            self.scale[self.n_cont:self.n_cont+self.n_cat_d] = \
-                self.cat_scale[:]
+        n_total = 0
+        # Calculate scaling for continuous variables
+        for i in range(self.n_cont):
+            self.scale[i] = self.cont_ub[i] - self.cont_lb[i]
+            self.scaled_des_tols[i] = (self.des_tols[self.des_order.index(i)] /
+                                       self.scale[i])
+        n_total = n_total + self.n_cont
+        # Calculate scaling for continuous variables
+        for i in range(self.n_int):
+            self.scale[n_total + i] = self.int_ub[i] - self.int_lb[i]
+            self.scaled_des_tols[n_total + i] = \
+                self.des_tols[self.des_order.index(i)] / self.scale[i]
+        n_total = n_total + self.n_int
+        # Calculate scaling for categorical variables
+        self.scale[n_total:n_total+self.n_cat_d] = self.cat_scale[:]
+        self.scaled_des_tols[n_total:n_total+self.n_cat_d] = \
+            self.cat_des_tols[:]
+        n_total += self.n_cat_d
+        # Calculate scaling for custom variables
+        self.scale[n_total:n_total+sum(self.n_custom_d)] = 1.0
+        self.scaled_des_tols[n_total:n_total+sum(self.n_custom_d)] = \
+            self.custom_des_tols[:]
+        n_total += sum(self.n_custom_d)
+        # Calculate scaling for raw variables
+        self.scale[n_total:n_total+self.n_raw] = 1.0
+        self.scaled_des_tols[n_total:n_total+self.n_raw] = 1.0e-8
+        self.scaled_lb[n_total:n_total+self.n_raw] = -np.inf
+        self.scaled_ub[n_total:n_total+self.n_raw] = np.inf
+        n_total += self.n_raw
         # Reset the database
         self.n_dat = 0
         self.data = {}
@@ -658,12 +895,13 @@ def addSimulation(self, *args):
         from parmoo.util import check_sims
 
         # Iterate through args to add each sim
-        check_sims(self.n_cont + self.n_cat, *args)
+        check_sims(self.n_cont + self.n_cat + self.n_int + self.n_custom +
+                   self.n_raw, *args)
         for arg in 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:
@@ -680,7 +918,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 = {}
@@ -698,7 +936,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
@@ -767,7 +1005,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
@@ -782,7 +1020,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:
@@ -836,7 +1074,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
@@ -851,7 +1089,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:
@@ -895,10 +1133,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")
@@ -910,10 +1148,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
@@ -967,12 +1207,14 @@ def getDesignType(self):
 
         """
 
-        if self.n_cont + self.n_cat < 1:
+        if self.n_cont + self.n_cat + self.n_int + self.n_custom + \
+           self.n_raw < 1:
             return None
         elif self.use_names:
             return np.dtype(self.des_names)
         else:
-            return np.dtype(('f8', (self.n_cont + self.n_cat,)))
+            return np.dtype(('f8', (self.n_cont + self.n_cat + self.n_int +
+                                    self.n_custom + self.n_raw,)))
 
     def getSimulationType(self):
         """ Get the numpy dtypes of the simulation outputs for this MOOP.
@@ -1248,7 +1490,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
@@ -1282,7 +1524,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)
@@ -1302,8 +1544,8 @@ def evaluateConstraints(self, x):
             # Return the constraint violations
             return cx
 
-    def evaluateLagrangian(self, x):
-        """ Evaluate the augmented Lagrangian using the surrogates as needed.
+    def evaluatePenalty(self, x):
+        """ Evaluate the penalized objective using the surrogates as needed.
 
         Warning: Not recommended for external usage!
 
@@ -1322,7 +1564,7 @@ def evaluateLagrangian(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
@@ -1341,13 +1583,13 @@ def evaluateLagrangian(self, x):
                                      self.__unpack_sim__(sim))
                 if cx > 0.0:
                     Lx[:] = Lx[:] + cx
-        # Compute the augmented Lagrangian
+        # Compute the penalized objective score
         Lx[:] = self.lam * Lx[:] + fx[:]
         # Return the result
         return Lx
 
     def evaluateGradients(self, x):
-        """ Evaluate the gradient of the augmented Lagrangian using surrogates.
+        """ Evaluate the gradient of the penalized objective using surrogates.
 
         Warning: Not recommended for external usage!
 
@@ -1366,7 +1608,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
@@ -1473,7 +1715,7 @@ def evaluateGradients(self, x):
                     dcx[:] = dcx[:] + dc_dx[i, :]
                     if self.m_total > 0:
                         dcx[:] = dcx[:] + np.dot(dc_dsim[i, :], dsim_dx[:, :])
-        # Construct the Jacobian of the augmented Lagrangian
+        # Construct the Jacobian of the penalized objective function
         dLx = np.zeros((self.o, self.n))
         for i in range(self.o):
             dLx[i, :] = dfx[i, :] + self.lam * dcx[:]
@@ -1614,13 +1856,13 @@ def iterate(self, k):
             # Add acquisition functions
             for i, acquisition in enumerate(self.acquisitions):
                 x0[i, :] = acquisition.setTarget(self.data,
-                                                 self.evaluateConstraints,
+                                                 self.evaluatePenalty,
                                                  self.history)
             # Set up the surrogate problem
             opt = self.optimizer(self.o, self.scaled_lb, self.scaled_ub,
                                  self.hyperparams)
             opt.setObjective(self.evaluateSurrogates)
-            opt.setLagrangian(self.evaluateLagrangian, self.evaluateGradients)
+            opt.setPenalty(self.evaluatePenalty, self.evaluateGradients)
             opt.setConstraints(self.evaluateConstraints)
             opt.addAcquisition(*self.acquisitions)
             opt.setReset(self.resetSurrogates)
@@ -1795,7 +2037,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...")
@@ -1803,6 +2045,9 @@ def solve(self, budget):
         logging.info(f"   {self.n} design dimensions")
         logging.info(f"     continuous design variables: {self.n_cont}")
         logging.info(f"     categorical design variables: {self.n_cat}")
+        logging.info(f"     integer design variables: {self.n_int}")
+        logging.info(f"     custom design variables: {self.n_custom}")
+        logging.info(f"     raw design variables: {self.n_raw}")
         logging.info(f"   {self.m_total} simulation outputs")
         logging.info(f"   {self.s} simulations")
         for i in range(self.s):
@@ -1850,7 +2095,7 @@ def solve(self, budget):
                      "iterations.")
         return
 
-    def getPF(self):
+    def getPF(self, format='ndarray'):
         """ Extract nondominated and efficient sets from internal databases.
 
         Returns:
@@ -1910,9 +2155,14 @@ def getPF(self):
                           'f_vals': pf['f_vals'].copy()}
             if self.p > 0:
                 result['c_vals'] = pf['c_vals'].copy()
-        return result
+        if format == 'pandas':
+            return pd.DataFrame(result)
+        elif format == 'ndarray':
+            return result
+        else:
+            raise ValueError(str(format) + " is an invalid value for 'format'")
 
-    def getSimulationData(self):
+    def getSimulationData(self, format='ndarray'):
         """ Extract all computed simulation outputs from the MOOP's database.
 
         Returns:
@@ -1961,7 +2211,13 @@ def getSimulationData(self):
                                                                            0]
                     else:
                         result[sname[0]]['out'] = self.sim_db[i]['s_vals']
-            return result
+            if format == 'pandas':
+                return pd.DataFrame(result)
+            elif format == 'ndarray':
+                return result
+            else:
+                raise ValueError(str(format) + "is an invalid value for "
+                                 + "'format'")
         else:
             # Initialize result list
             result = []
@@ -1976,9 +2232,15 @@ def getSimulationData(self):
                 else:
                     result.append({'x_vals': np.zeros(0),
                                    's_vals': np.zeros(0)})
-            return result
+            if format == 'pandas':
+                return pd.DataFrame(result)
+            elif format == 'ndarray':
+                return result
+            else:
+                raise ValueError(str(format) + " is an invalid value for "
+                                 + "'format'")
 
-    def getObjectiveData(self):
+    def getObjectiveData(self, format='ndarray'):
         """ Extract all computed objective scores from this MOOP's database.
 
         Returns:
@@ -2031,7 +2293,12 @@ def getObjectiveData(self):
                           'f_vals': self.data['f_vals'].copy()}
                 if self.p > 0:
                     result['c_vals'] = self.data['c_vals'].copy()
-        return result
+        if format == 'pandas':
+            return pd.DataFrame(result)
+        elif format == 'ndarray':
+            return result
+        else:
+            raise ValueError(str(format) + " is an invalid value for 'format'")
 
     def save(self, filename="parmoo"):
         """ Serialize and save the MOOP object and all of its dependencies.
@@ -2069,11 +2336,17 @@ def save(self, filename="parmoo"):
                         'p': self.p,
                         's': self.s,
                         'n_dat': self.n_dat,
-                        'lb': self.lb,
-                        'ub': self.ub,
+                        'cont_lb': self.cont_lb,
+                        'cont_ub': self.cont_ub,
+                        'int_lb': self.int_lb,
+                        'int_ub': self.int_ub,
                         'n_cat_d': self.n_cat_d,
+                        'n_custom_d': self.n_custom_d,
                         'n_cat': self.n_cat,
                         'n_cont': self.n_cont,
+                        'n_int': self.n_int,
+                        'n_custom': self.n_custom,
+                        'n_raw': self.n_raw,
                         'n_lvls': self.n_lvls,
                         'des_order': self.des_order,
                         'cat_names': self.cat_names,
@@ -2083,6 +2356,7 @@ def save(self, filename="parmoo"):
                         'const_names': self.const_names,
                         'lam': self.lam,
                         'des_tols': self.des_tols,
+                        'epsilon': self.epsilon,
                         'hyperparams': self.hyperparams,
                         'history': self.history,
                         'use_names': self.use_names,
@@ -2111,6 +2385,10 @@ def save(self, filename="parmoo"):
             parmoo_state['cat_des_tols'] = self.cat_des_tols.tolist()
         else:
             parmoo_state['cat_des_tols'] = self.cat_des_tols
+        if isinstance(self.custom_des_tols, np.ndarray):
+            parmoo_state['custom_des_tols'] = self.custom_des_tols.tolist()
+        else:
+            parmoo_state['custom_des_tols'] = self.custom_des_tols
         if isinstance(self.cat_lb, np.ndarray):
             parmoo_state['cat_lb'] = self.cat_lb.tolist()
         else:
@@ -2129,11 +2407,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:
@@ -2176,6 +2454,12 @@ def save(self, filename="parmoo"):
                                                     ci.__class__.__module__))
                 parmoo_state['constraints_info'].append(
                         codecs.encode(pickle.dumps(ci), "base64").decode())
+        # Store names/modules of custom embedders
+        parmoo_state['custom_embedders'] = [(ei.__name__, ei.__module__)
+                                            for ei in self.custom_embedders]
+        # Store names/modules of custom extracters
+        parmoo_state['custom_extracters'] = [(ei.__name__, ei.__module__)
+                                             for ei in self.custom_extracters]
         # Store names/modules of object classes
         parmoo_state['optimizer'] = (self.optimizer.__name__,
                                      self.optimizer.__module__)
@@ -2260,11 +2544,17 @@ def load(self, filename="parmoo"):
         self.p = parmoo_state['p']
         self.s = parmoo_state['s']
         self.n_dat = parmoo_state['n_dat']
-        self.lb = parmoo_state['lb']
-        self.ub = parmoo_state['ub']
+        self.cont_lb = parmoo_state['cont_lb']
+        self.cont_ub = parmoo_state['cont_ub']
+        self.int_lb = parmoo_state['int_lb']
+        self.int_ub = parmoo_state['int_ub']
         self.n_cat_d = parmoo_state['n_cat_d']
+        self.n_custom_d = parmoo_state['n_custom_d']
         self.n_cat = parmoo_state['n_cat']
         self.n_cont = parmoo_state['n_cont']
+        self.n_int = parmoo_state['n_int']
+        self.n_custom = parmoo_state['n_custom']
+        self.n_raw = parmoo_state['n_raw']
         self.n_lvls = parmoo_state['n_lvls']
         self.des_order = parmoo_state['des_order']
         self.cat_names = parmoo_state['cat_names']
@@ -2275,6 +2565,7 @@ def load(self, filename="parmoo"):
                             for item in parmoo_state['const_names']]
         self.lam = parmoo_state['lam']
         self.des_tols = parmoo_state['des_tols']
+        self.epsilon = parmoo_state['epsilon']
         self.hyperparams = parmoo_state['hyperparams']
         self.history = parmoo_state['history']
         self.use_names = parmoo_state['use_names']
@@ -2288,17 +2579,18 @@ def load(self, filename="parmoo"):
         self.scaled_ub = np.array(parmoo_state['scaled_ub'])
         self.scaled_des_tols = np.array(parmoo_state['scaled_des_tols'])
         self.cat_des_tols = np.array(parmoo_state['cat_des_tols'])
+        self.custom_des_tols = np.array(parmoo_state['custom_des_tols'])
         self.cat_lb = np.array(parmoo_state['cat_lb'])
         self.cat_scale = np.array(parmoo_state['cat_scale'])
         self.RSVT = np.array(parmoo_state['RSVT'])
         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']:
@@ -2386,6 +2678,18 @@ def load(self, filename="parmoo"):
             else:
                 toadd = pickle.loads(codecs.decode(info.encode(), "base64"))
             self.constraints.append(toadd)
+        # Recover custom embedders
+        self.custom_embedders = []
+        for i, (e_name, e_mod) in enumerate(parmoo_state['custom_embedders']):
+            mod = import_module(e_mod)
+            new_em = getattr(mod, e_name)
+            self.custom_embedders.append(new_em)
+        # Recover custom extracters
+        self.custom_extracters = []
+        for i, (e_name, e_mod) in enumerate(parmoo_state['custom_extracters']):
+            mod = import_module(e_mod)
+            new_em = getattr(mod, e_name)
+            self.custom_extracters.append(new_em)
         # Recover object classes
         mod = import_module(parmoo_state['optimizer'][1])
         self.optimizer = getattr(mod, parmoo_state['optimizer'][0])
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 564f355..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):
@@ -105,11 +104,11 @@ def setReset(self, reset):
 
         return
 
-    def setLagrangian(self, lagrangian, grad_func):
+    def setPenalty(self, penalty_func, grad_func):
         """ Add a matrix-valued gradient function for obj_func.
 
         Args:
-            lagrangian (function): A vector-valued augmented Lagrangian
+            penalty_func (function): A vector-valued penalized objective
                 that incorporates a penalty for violating constraints.
 
             grad_func (function): A matrix-valued function that can be
@@ -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):
@@ -354,11 +351,11 @@ def setReset(self, reset):
 
         return
 
-    def setLagrangian(self, lagrangian, grad_func):
+    def setPenalty(self, penalty_func, grad_func):
         """ Add a matrix-valued gradient function for obj_func.
 
         Args:
-            lagrangian (function): A vector-valued augmented Lagrangian
+            penalty_func (function): A vector-valued penalized objective
                 that incorporates a penalty for violating constraints.
 
             grad_func (function): A matrix-valued function that can be
@@ -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 47751b4..2d13d99 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):
@@ -29,10 +30,10 @@ class LBFGSB(SurrogateOptimizer):
 
     # Slots for the LBFGSB class
     __slots__ = ['n', 'bounds', 'acquisitions', 'budget', 'constraints',
-                 'objectives', 'gradients', 'lagrangian']
+                 'objectives', 'gradients', 'penalty_func', 'restarts']
 
     def __init__(self, o, lb, ub, hyperparams):
-        """ Constructor for the LocalGPS class.
+        """ Constructor for the LBFGSB class.
 
         Args:
             o (int): The number of objectives.
@@ -46,22 +47,35 @@ def __init__(self, o, lb, ub, hyperparams):
 
             hyperparams (dict): A dictionary of hyperparameters for the
                 optimization procedure. It may contain the following:
-                 * opt_budget (int): The evaluation budget (default: 10,000).
+                 * opt_budget (int): The evaluation budget per solve
+                   (default: 1000).
+                 * opt_restarts (int): Number of multisolve restarts per
+                   scalarization (default: n+1).
 
         Returns:
             SurrogateOptimizer: A new SurrogateOptimizer object.
 
         """
 
-        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
         self.bounds[:, 1] = ub
         # Check that the contents of hyperparams is legal
+        if 'opt_restarts' in hyperparams:
+            if isinstance(hyperparams['opt_restarts'], int):
+                if hyperparams['opt_restarts'] < 1:
+                    raise ValueError("hyperparams['opt_restarts'] "
+                                     "must be positive")
+                else:
+                    self.restarts = hyperparams['opt_restarts']
+            else:
+                raise TypeError("hyperparams['opt_restarts'] "
+                                 "must be an integer")
+        else:
+            self.restarts = self.n + 1
         if 'opt_budget' in hyperparams:
             if isinstance(hyperparams['opt_budget'], int):
                 if hyperparams['opt_budget'] < 1:
@@ -70,10 +84,10 @@ 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
+            self.budget = 1000
         self.acquisitions = []
         return
 
@@ -94,7 +108,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):
@@ -106,36 +120,36 @@ def setReset(self, reset):
 
         return
 
-    def setLagrangian(self, lagrangian, grad_func):
+    def setPenalty(self, penalty_func, grad_func):
         """ Add a matrix-valued gradient function for obj_func.
 
         Args:
-            lagrangian (function): A vector-valued augmented Lagrangian
+            penalty_func (function): A vector-valued penalized objective
                 that incorporates a penalty for violating constraints.
 
             grad_func (function): A matrix-valued function that can be
-                evaluated to obtain the Jacobian matrix for the Lagrangian.
+                evaluated to obtain the Jacobian matrix for obj_func.
 
         """
 
         # Check whether grad_func() has an appropriate signature
         if callable(grad_func):
             if len(inspect.signature(grad_func).parameters) != 1:
-                raise ValueError("grad_func() must accept exactly one input")
+                raise ValueError("grad_func must accept exactly one input")
             else:
                 # Add grad_func to the problem
                 self.gradients = grad_func
         else:
-            raise ValueError("grad_func() must be callable")
-        # Check whether lagrangian() has an appropriate signature
-        if callable(lagrangian):
-            if len(inspect.signature(lagrangian).parameters) != 1:
-                raise ValueError("lagrangian() must accept exactly one input")
+            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:
+                raise ValueError("penalty_func must accept exactly one input")
             else:
                 # Add Lagrangian to the problem
-                self.lagrangian = lagrangian
+                self.penalty_func = penalty_func
         else:
-            raise ValueError("lagrangian() must be callable")
+            raise TypeError("penalty_func must be callable")
         return
 
     def setConstraints(self, constraint_func):
@@ -158,7 +172,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 +187,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,8 +197,8 @@ def solve(self, x):
         """ Solve the surrogate problem using L-BFGS-B.
 
         Args:
-            x (np.ndarray): A 2d array containing a list of feasible
-                design points used to warm start the search.
+            x (np.ndarray): A 2d array containing a list of design points
+                used to warm start the search.
 
         Returns:
             np.ndarray: A 2d numpy.ndarray of potentially efficient design
@@ -202,7 +216,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 \
@@ -210,25 +224,47 @@ def solve(self, x):
                 raise ValueError("some of starting points (x) are infeasible")
         # Initialize an empty list of results
         result = []
-        # Calculate budget per call
-        budget_per_call = self.budget / len(self.acquisitions)
         # For each acqusisition function
         for j, acquisition in enumerate(self.acquisitions):
 
             # Define the scalarized wrapper functions
             def scalar_f(x, *args):
-                return acquisition.scalarize(self.lagrangian(x))
+                return acquisition.scalarize(self.penalty_func(x))
 
             def scalar_g(x, *args):
-                return acquisition.scalarizeGrad(self.lagrangian(x),
+                return acquisition.scalarizeGrad(self.penalty_func(x),
                                                  self.gradients(x))
 
-            # Get the solution
-            res = optimize.minimize(scalar_f, x[j, :], method='L-BFGS-B',
-                                    jac=scalar_g, bounds=self.bounds,
-                                    options={'maxiter': budget_per_call})
+            # Get the solution via multistart solve
+            soln = x[j, :].copy()
+            for i in range(self.restarts):
+                if i == 0:
+                    # Use center point to warm-start first start
+                    x0 = x[j, :].copy()
+                elif i == 1:
+                    # Use predicted gradient step to warm-start second start
+                    x0 = x[j, :].copy()
+                    gg = scalar_g(x0)
+                    for ii in range(self.n):
+                        if gg[ii] < 0:
+                            x0[ii] = self.bounds[ii, 1]
+                        elif gg[ii] > 0:
+                            x0[ii] = self.bounds[ii, 0]
+                else:
+                    # Random starting point within bounds for all other starts
+                    x0 = (np.random.random_sample(self.n) *
+                          (self.bounds[:, 1] - self.bounds[:, 0]) +
+                          self.bounds[:, 0])
+
+                # Solve the problem globally within bound constraints
+                res = optimize.minimize(scalar_f, x0, method='L-BFGS-B',
+                                        jac=scalar_g, bounds=self.bounds,
+                                        options={'maxiter': self.budget})
+                if scalar_f(res['x']) < scalar_f(soln):
+                    soln = res['x']
+
             # Append the found minima to the results list
-            result.append(res['x'])
+            result.append(soln)
         return np.asarray(result)
 
 
@@ -243,10 +279,11 @@ class TR_LBFGSB(SurrogateOptimizer):
 
     # Slots for the LBFGSB class
     __slots__ = ['n', 'bounds', 'acquisitions', 'budget', 'constraints',
-                 'objectives', 'gradients', 'lagrangian', 'resetObjectives']
+                 'objectives', 'gradients', 'penalty_func', 'resetObjectives',
+                 'restarts']
 
     def __init__(self, o, lb, ub, hyperparams):
-        """ Constructor for the LocalGPS class.
+        """ Constructor for the TR_LBFGSB class.
 
         Args:
             o (int): The number of objectives.
@@ -260,17 +297,18 @@ def __init__(self, o, lb, ub, hyperparams):
 
             hyperparams (dict): A dictionary of hyperparameters for the
                 optimization procedure. It may contain the following:
-                 * opt_budget (int): The evaluation budget (default: 10,000).
+                 * opt_budget (int): The evaluation budget per solve
+                   (default: 1000).
+                 * opt_restarts (int): Number of multisolve restarts per
+                   scalarization (default: 2).
 
         Returns:
             SurrogateOptimizer: A new SurrogateOptimizer object.
 
         """
 
-        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,10 +322,23 @@ 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
+            self.budget = 1000
+        # Check that the contents of hyperparams is legal
+        if 'opt_restarts' in hyperparams:
+            if isinstance(hyperparams['opt_restarts'], int):
+                if hyperparams['opt_restarts'] < 1:
+                    raise ValueError("hyperparams['opt_restarts'] "
+                                     "must be positive")
+                else:
+                    self.restarts = hyperparams['opt_restarts']
+            else:
+                raise TypeError("hyperparams['opt_restarts'] "
+                                 "must be an integer")
+        else:
+            self.restarts = 2
         self.acquisitions = []
         return
 
@@ -308,7 +359,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,18 +380,18 @@ 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 setLagrangian(self, lagrangian, grad_func):
+    def setPenalty(self, penalty_func, grad_func):
         """ Add a matrix-valued gradient function for obj_func.
 
         Args:
-            lagrangian (function): A vector-valued augmented Lagrangian
+            penalty_func (function): A vector-valued penalized objective
                 that incorporates a penalty for violating constraints.
 
             grad_func (function): A matrix-valued function that can be
-                evaluated to obtain the Jacobian matrix for the Lagrangian.
+                evaluated to obtain the Jacobian matrix for obj_func.
 
         """
 
@@ -352,16 +403,16 @@ def setLagrangian(self, lagrangian, grad_func):
                 # Add grad_func to the problem
                 self.gradients = grad_func
         else:
-            raise ValueError("grad_func() must be callable")
-        # Check whether lagrangian() has an appropriate signature
-        if callable(lagrangian):
-            if len(inspect.signature(lagrangian).parameters) != 1:
-                raise ValueError("lagrangian() must accept exactly one input")
+            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:
+                raise ValueError("penalty_func must accept exactly one input")
             else:
                 # Add Lagrangian to the problem
-                self.lagrangian = lagrangian
+                self.penalty_func = penalty_func
         else:
-            raise ValueError("lagrangian() must be callable")
+            raise TypeError("penalty_func must be callable")
         return
 
     def setConstraints(self, constraint_func):
@@ -384,7 +435,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 +450,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)
@@ -409,8 +460,8 @@ def solve(self, x):
         """ Solve the surrogate problem using L-BFGS-B.
 
         Args:
-            x (np.ndarray): A 2d array containing a list of feasible
-                design points used to warm start the search.
+            x (np.ndarray): A 2d array containing a list of design points
+                used to warm start the search.
 
         Returns:
             np.ndarray: A 2d numpy.ndarray of potentially efficient design
@@ -428,7 +479,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 \
@@ -436,17 +487,15 @@ def solve(self, x):
                 raise ValueError("some of starting points (x) are infeasible")
         # Initialize an empty list of results
         result = []
-        # Calculate budget per call
-        budget_per_call = self.budget / len(self.acquisitions)
         # For each acqusisition function
         for j, acquisition in enumerate(self.acquisitions):
 
             # Define the scalarized wrapper functions
             def scalar_f(x, *args):
-                return acquisition.scalarize(self.lagrangian(x))
+                return acquisition.scalarize(self.penalty_func(x))
 
             def scalar_g(x, *args):
-                return acquisition.scalarizeGrad(self.lagrangian(x),
+                return acquisition.scalarizeGrad(self.penalty_func(x),
                                                  self.gradients(x))
 
             # Create a new trust region
@@ -455,10 +504,33 @@ def scalar_g(x, *args):
             for i in range(self.n):
                 bounds[i, 0] = max(self.bounds[i, 0], x[j, i] - rad)
                 bounds[i, 1] = min(self.bounds[i, 1], x[j, i] + rad)
-            # Get the solution
-            res = optimize.minimize(scalar_f, x[j, :], method='L-BFGS-B',
-                                    jac=scalar_g, bounds=bounds,
-                                    options={'maxiter': budget_per_call})
+
+            # Get the solution via multistart solve
+            soln = x[j, :].copy()
+            for i in range(self.restarts):
+                if i == 0:
+                    # Use center point to warm-start first start
+                    x0 = x[j, :].copy()
+                elif i == 1:
+                    # Use predicted gradient step to warm-start second start
+                    x0 = x[j, :].copy()
+                    gg = scalar_g(x0)
+                    for ii in range(self.n):
+                        if gg[ii] < 0:
+                            x0[ii] = bounds[ii, 1]
+                        elif gg[ii] > 0:
+                            x0[ii] = bounds[ii, 0]
+                else:
+                    # Random starting point within bounds for all other starts
+                    x0 = (np.random.random_sample(self.n) *
+                          (bounds[:, 1] - bounds[:, 0]) + bounds[:, 0])
+
+                # Solve the problem within the local trust region
+                res = optimize.minimize(scalar_f, x0, method='L-BFGS-B',
+                                        jac=scalar_g, bounds=bounds,
+                                        options={'maxiter': self.budget})
+                if scalar_f(res['x']) < scalar_f(soln):
+                    soln = res['x']
             # Append the found minima to the results list
-            result.append(res['x'])
+            result.append(soln)
         return np.asarray(result)
diff --git a/parmoo/optimizers/random_search.py b/parmoo/optimizers/random_search.py
index d8b22ee..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):
@@ -104,11 +103,11 @@ def setReset(self, reset):
 
         return
 
-    def setLagrangian(self, lagrangian, grad_func):
+    def setPenalty(self, penalty_func, grad_func):
         """ Add a matrix-valued gradient function for obj_func.
 
         Args:
-            lagrangian (function): A vector-valued augmented Lagrangian
+            penalty_func (function): A vector-valued penalized objective
                 that incorporates a penalty for violating constraints.
 
             grad_func (function): A matrix-valued function that can be
@@ -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..7bbd025 100644
--- a/parmoo/surrogates/gaussian_proc.py
+++ b/parmoo/surrogates/gaussian_proc.py
@@ -13,6 +13,8 @@
 import numpy as np
 from parmoo.structs import SurrogateFunction
 from scipy.spatial.distance import cdist
+from scipy.stats import tstd
+from parmoo.util import xerror
 
 
 class GaussRBF(SurrogateFunction):
@@ -52,10 +54,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 +67,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 +78,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 +115,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 +171,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 +238,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 +264,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 +296,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")
@@ -315,22 +315,26 @@ def improve(self, x, global_improv):
                 x_new[:] = self.lb[:] + (np.random.random(self.n)
                                          * (self.ub[:] - self.lb[:]))
         else:
-            # Find the n+1 closest points to x in the current database.
-            dists = np.asarray([np.amax(np.abs(x - xj) / self.eps)
+            # Find the n+1 closest points to x in the current database
+            diffs = np.asarray([np.abs(x - xj) / self.eps
                                 for xj in self.x_vals])
+            dists = np.asarray([np.amax(dj) for dj in diffs])
             inds = np.argsort(dists)
-            if dists[inds[self.n]] >= 1.5:
-                # Uniformly sample within the box [x - rad, x + rad].
-                rad = np.abs(x - self.x_vals[self.n])
-                xn = self.x_vals[self.n]
-                x_new = np.fmin(np.fmax(2.0 * np.random.random(self.n)
-                                        * rad[:] + (x - np.abs(xn)),
-                                        self.lb), self.ub)
+            diffs = diffs[inds]
+            if dists[inds[self.n]] > 1.5:
+                # Calculate the normalized sample std dev along each axis
+                stddev = np.asarray(tstd(diffs[:self.n+1], axis=0))
+                stddev[:] = np.maximum(stddev, np.ones(self.n))
+                stddev[:] = stddev[:] / np.amin(stddev)
+                # Sample within B(x, dists[inds[self.n]] / stddev)
+                rad = (dists[inds[self.n]] * self.eps) / stddev
+                x_new = np.fmin(np.fmax(2.0 * (np.random.random(self.n) - 0.5)
+                                        * rad[:] + x, self.lb), self.ub)
                 while any([np.all(np.abs(x_new - xj) < self.eps)
                            for xj in self.x_vals]):
-                    x_new = np.fmin(np.fmax(2.0 * np.random.random(self.n)
-                                            * rad[:] + (x - np.abs(xn)),
-                                            self.lb), self.ub)
+                    x_new = np.fmin(np.fmax(2.0 *
+                                            (np.random.random(self.n) - 0.5)
+                                            * rad[:] + x, self.lb), self.ub)
             else:
                 # If the n+1st nearest point is too close, use global_improv.
                 x_new[:] = self.lb[:] + np.random.random(self.n) \
@@ -437,10 +441,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 +457,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 +469,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 +481,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 +518,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 +550,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 +583,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 +641,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 +668,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,14 +701,14 @@ 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")
             elif (np.any(x < self.lb - self.eps) or
                   np.any(x > self.ub + self.eps)):
                 raise ValueError("x cannot be infeasible")
-        # Allocate the output array.
+        # Allocate the output array
         x_new = np.zeros(self.n)
         if global_improv:
             # If global improvement has been specified, randomly select a
@@ -719,27 +721,30 @@ def improve(self, x, global_improv):
                                          * (self.ub[:] - self.lb[:]))
         else:
             # Find the n_loc closest points to x in the current database
-            dists = np.asarray([np.amax(np.abs(x - xj) / self.eps)
+            diffs = np.asarray([np.abs(x - xj) / self.eps
                                 for xj in self.x_vals])
+            dists = np.asarray([np.amax(dj) for dj in diffs])
             inds = np.argsort(dists)
+            diffs = diffs[inds]
             if dists[inds[self.n_loc - 1]] > 1.5:
-                # Uniformly sample within B(x, dists[n_loc]).
-                xn = self.x_vals[self.n_loc - 1]
-                rad = np.abs(x - self.x_vals[self.n_loc - 1])
-                x_new = np.fmin(np.fmax(2.0 * np.random.random(self.n)
-                                        * rad[:] + (x - np.abs(xn)),
-                                        self.lb), self.ub)
+                # Calculate the normalized sample std dev along each axis
+                stddev = np.asarray(tstd(diffs[:self.n_loc], axis=0))
+                stddev[:] = np.maximum(stddev, np.ones(self.n))
+                stddev[:] = stddev[:] / np.amin(stddev)
+                # Sample within B(x, dists[inds[n_loc - 1]] / stddev)
+                rad = (dists[inds[self.n_loc - 1]] * self.eps) / stddev
+                x_new = np.fmin(np.fmax(2.0 * (np.random.random(self.n) - 0.5)
+                                        * rad[:] + x, self.lb), self.ub)
                 while any([np.all(np.abs(x_new - xj) < self.eps)
                            for xj in self.x_vals]):
-                    x_new = np.fmin(np.fmax(2.0 * rad[:] *
-                                            np.random.random(self.n) +
-                                            (x - np.abs(xn)),
-                                            self.lb), self.ub)
+                    x_new = np.fmin(np.fmax(2.0 *
+                                            (np.random.random(self.n) - 0.5)
+                                            * rad[:] + x, self.lb), self.ub)
             else:
                 # If the n_loc nearest point is too close, use global_improv
                 x_new[:] = self.lb[:] + np.random.random(self.n) \
                            * (self.ub[:] - self.lb[:])
-                # If the nearest point is too close, resample.
+                # If the nearest point is too close, resample
                 while any([np.all(np.abs(x_new - xj) < self.eps)
                            for xj in self.x_vals]):
                     x_new[:] = self.lb[:] + (np.random.random(self.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'"
diff --git a/parmoo/version.py b/parmoo/version.py
index b794fd4..7fd229a 100644
--- a/parmoo/version.py
+++ b/parmoo/version.py
@@ -1,1 +1,1 @@
-__version__ = '0.1.0'
+__version__ = '0.2.0'
diff --git a/parmoo/viz/__init__.py b/parmoo/viz/__init__.py
new file mode 100644
index 0000000..69b8569
--- /dev/null
+++ b/parmoo/viz/__init__.py
@@ -0,0 +1,20 @@
+from .plot import (
+    scatter,
+    parallel_coordinates,
+    radar,
+)
+from .graph import (
+    generate_scatter,
+    generate_parallel,
+    generate_radar,
+)
+from .utilities import (
+    export_file,
+    set_plot_name,
+    set_database,
+    set_hover_info,
+    check_inputs,
+)
+from .dashboard import (
+    Dash_App,
+)
diff --git a/parmoo/viz/dashboard.py b/parmoo/viz/dashboard.py
new file mode 100644
index 0000000..c96ab5f
--- /dev/null
+++ b/parmoo/viz/dashboard.py
@@ -0,0 +1,1014 @@
+""" This module contains private methods for hosting and receiving callbacks
+from an interactiv dashboard. This module is intended only for developer use.
+
+Note that some docstrings may be incomplete.
+
+"""
+
+import pandas as pd
+import plotly.io as pio
+from os import environ
+from webbrowser import open_new
+from warnings import warn
+import logging
+from dash import (
+    Dash,
+    Input,
+    Output,
+    html,
+    dcc,
+    exceptions,
+    callback_context,
+    no_update,
+)
+from .graph import (
+    generate_scatter,
+    generate_parallel,
+    generate_radar,
+)
+from .utilities import (
+    set_plot_name,
+    set_database,
+)
+
+
+class Dash_App:
+    """ A class for hosting the dashboard app. """
+
+    def __init__(
+        self,
+        moop,
+        plot_type,
+        db,
+        points,
+        height,
+        width,
+        font,
+        fontsize,
+        background_color,
+        screenshot,
+        image_export_format,
+        data_export_format,
+        dev_mode,
+        pop_up,
+        port,
+    ):
+        """ Constructor for dashboard app. """
+
+        logging.info('initializing dashboard')
+
+        # ! STATE
+
+        # * define independent state
+        self.moop = moop
+        self.plot_type = plot_type
+        self.db = db
+        self.points = points
+        self.height = height
+        self.width = width
+        self.font = font
+        self.fontsize = fontsize
+        self.background_color = background_color
+        self.screenshot = screenshot
+        self.image_export_format = image_export_format
+        self.data_export_format = data_export_format
+        self.dev_mode = dev_mode
+        self.pop_up = pop_up
+        self.port = port
+
+        # * define dependent state
+        self.selection_indexes = []
+        self.constraint_range = self.set_constraint_range(None)
+        self.plot_name = set_plot_name(db=self.db)
+        self.database = set_database(moop, db=self.db, points=self.points)
+        self.graph = self.generate_graph()
+        self.config = self.configure()
+
+    # ! LAYOUT
+
+        # * initialize app
+        app = Dash(__name__)
+        # * lay out app
+        app.layout = html.Div(children=[
+            dcc.Dropdown(
+                id='plot_type_dropdown',
+                options=['Scatterplot',
+                         'Parallel Coordinates plot',
+                         'Radar plot'],
+                placeholder='Change plot type',
+            ),
+            dcc.Dropdown(
+                id='database_dropdown',
+                options=[
+                    'Pareto Front',
+                    'Objective Data'
+                ],
+                placeholder='Change dataset',
+            ),
+            html.Button(
+                children='Export all data',
+                id='download_dataset_button',
+            ),
+            dcc.Download(
+                id='download_dataset_dcc',
+            ),
+            html.Button(
+                children='Export selected data',
+                id='download_selection_button',
+            ),
+            dcc.Download(
+                id='download_selection_dcc',
+            ),
+            html.Button(
+                children='Export image to working directory',
+                id='download_image_button',
+            ),
+            dcc.Store(
+                id='image'
+            ),
+            dcc.Checklist(
+                id='constraint_checkboxes',
+                options=[
+                    {'label': 'show constraint-satisfying points',
+                     'value': 'constraint_satisfying'},
+                    {'label': 'show constraint-violating points',
+                     'value': 'constraint_violating'},
+                ],
+                value=['constraint_satisfying'],
+                inline=True,
+            ),
+            html.Br(),
+            # * main plot
+            dcc.Graph(
+                id='parmoo_graph',
+                figure=self.graph,
+                config=self.config,
+            ),
+            html.Br(),
+            html.Button(
+                children='Show graph customization options',
+                id='show_customization_options',
+            ),
+            html.Button(
+                children='Hide graph customization options',
+                id='hide_customization_options',
+                style=dict(display='none'),
+            ),
+            html.Button(
+                children='Show export options',
+                id='show_export_options',
+            ),
+            html.Button(
+                children='Hide export options',
+                id='hide_export_options',
+                style=dict(display='none'),
+            ),
+            dcc.Store(
+                id='selection',
+            ),
+            dcc.Input(
+                id='font_selection_input',
+                placeholder='Set font',
+                type='text',
+                value='',
+                debounce=True,
+                style=dict(display='none'),
+            ),
+            dcc.Input(
+                id='font_size_input',
+                placeholder='Set font size',
+                type='number',
+                value='',
+                min=1,
+                max=100,
+                debounce=True,
+                style=dict(display='none'),
+            ),
+            dcc.Input(
+                id='graph_width_input',
+                placeholder='Set graph width',
+                type='number',
+                value='',
+                min=10,
+                step=1,
+                style=dict(display='none'),
+            ),
+            dcc.Input(
+                id='graph_height_input',
+                placeholder='Set graph height',
+                type='number',
+                value='',
+                min=10,
+                step=1,
+                style=dict(display='none'),
+            ),
+            dcc.Input(
+                id='plot_name_input',
+                placeholder='Set plot name',
+                type='text',
+                value='',
+                debounce=True,
+                style=dict(display='none'),
+            ),
+            dcc.Dropdown(
+                id='background_color_dropdown',
+                options=[
+                    'White',
+                    'Grey',
+                    'Black',
+                    'Transparent',
+                ],
+                placeholder='Set background color',
+                style=dict(display='none'),
+            ),
+            dcc.Dropdown(
+                id='image_export_format_dropdown',
+                options=[
+                    'PNG',
+                    'JPEG',
+                    'PDF',
+                    'SVG',
+                    'EPS',
+                    'HTML',
+                    'WebP'
+                ],
+                placeholder='Set image export format',
+                style=dict(display='none'),
+            ),
+            dcc.Store(
+                id='image_export_format_store',
+                storage_type='local',
+            ),
+            dcc.Dropdown(
+                id='data_export_format_dropdown',
+                options=[
+                    'CSV',
+                    'JSON',
+                ],
+                placeholder='Set data export format',
+                style=dict(display='none'),
+            ),
+            dcc.Store(
+                id='data_export_format_store',
+                storage_type='local',
+            )
+        ])
+
+    # ! CALLBACKS
+
+        # * show customization options
+        @app.callback(
+            Output(
+                component_id='show_customization_options',
+                component_property='style',),
+            Output(
+                component_id='hide_customization_options',
+                component_property='style',),
+            Output(
+                component_id='font_selection_input',
+                component_property='style',),
+            Output(
+                component_id='font_size_input',
+                component_property='style',),
+            Output(
+                component_id='graph_width_input',
+                component_property='style',),
+            Output(
+                component_id='graph_height_input',
+                component_property='style',),
+            Output(
+                component_id='plot_name_input',
+                component_property='style',),
+            Output(
+                component_id='background_color_dropdown',
+                component_property='style',),
+            Input(
+                component_id='show_customization_options',
+                component_property='n_clicks',),
+            Input(
+                component_id='hide_customization_options',
+                component_property='n_clicks',),
+            prevent_initial_call=True
+        )
+        def update_customization_components(
+            s_clicks,
+            h_clicks,
+        ):
+            """ Documentation incomplete. """
+
+            triggered_id = callback_context.triggered[0]['prop_id']
+            if triggered_id == 'show_customization_options.n_clicks':
+                logging.info("'show_customization_options.n_clicks' triggered")
+                return self.evaluate_customization_options('show', s_clicks)
+            elif triggered_id == 'hide_customization_options.n_clicks':
+                logging.info("'hide_customization_options.n_clicks' triggered")
+                return self.evaluate_customization_options('hide', h_clicks)
+
+        # * show export options
+        @app.callback(
+            Output(
+                component_id='show_export_options',
+                component_property='style',),
+            Output(
+                component_id='hide_export_options',
+                component_property='style',),
+            Output(
+                component_id='image_export_format_dropdown',
+                component_property='style',),
+            Output(
+                component_id='data_export_format_dropdown',
+                component_property='style',),
+            Input(
+                component_id='show_export_options',
+                component_property='n_clicks',),
+            Input(
+                component_id='hide_export_options',
+                component_property='n_clicks',),
+            prevent_initial_call=True
+        )
+        def update_export_components(
+            s_clicks,
+            h_clicks,
+        ):
+            """ Documentation incomplete. """
+
+            triggered_id = callback_context.triggered[0]['prop_id']
+            if triggered_id == 'show_export_options.n_clicks':
+                logging.info("'show_export_options.n_clicks' triggered")
+                return self.evaluate_export_options('show', s_clicks)
+            elif triggered_id == 'hide_export_options.n_clicks':
+                logging.info("'hide_export_options.n_clicks' triggered")
+                return self.evaluate_export_options('hide', h_clicks)
+
+        # * regenerate or update graph
+        # TODO whenever possible, update instead of regenerating
+        @app.callback(
+            Output(
+                component_id='parmoo_graph',
+                component_property='figure',),
+            # height - update
+            Input(
+                component_id='graph_height_input',
+                component_property='value',),
+            # width - update
+            Input(
+                component_id='graph_width_input',
+                component_property='value',),
+            # font - update
+            Input(
+                component_id='font_selection_input',
+                component_property='value',),
+            # font size - update
+            Input(
+                component_id='font_size_input',
+                component_property='value',),
+            # background color - update
+            Input(
+                component_id='background_color_dropdown',
+                component_property='value',),
+            # plot name - update
+            Input(
+                component_id='plot_name_input',
+                component_property='value',),
+            # plot type - regenerate
+            Input(
+                component_id='plot_type_dropdown',
+                component_property='value',),
+            # database - regenerate
+            Input(
+                component_id='database_dropdown',
+                component_property='value',),
+            # constraint showr - regenerate
+            Input(
+                component_id='constraint_checkboxes',
+                component_property='value'),
+            prevent_initial_call=True
+        )
+        def update_graph(
+            height_value,
+            width_value,
+            font_value,
+            font_size_value,
+            background_color_value,
+            plot_name_value,
+            plot_type_value,
+            database_value,
+            constraint_showr_value,
+        ):
+            """ Documentation incomplete. """
+
+            triggered_id = callback_context.triggered[0]['prop_id']
+            if 'graph_height_input.value' == triggered_id:
+                logging.info("'graph_height_input.value' triggered")
+                return self.evaluate_height(height_value)
+            elif 'graph_width_input.value' == triggered_id:
+                logging.info("'graph_width_input.value' triggered")
+                return self.evaluate_width(width_value)
+            elif 'font_selection_input.value' == triggered_id:
+                logging.info("'font_selection_input.value' triggered")
+                return self.evaluate_font(font_value)
+            elif 'font_size_input.value' == triggered_id:
+                logging.info("'font_size_input.value' triggered")
+                return self.evaluate_font_size(font_size_value)
+            elif 'background_color_dropdown.value' == triggered_id:
+                logging.info("'background_color_dropdown.value' triggered")
+                return self.evaluate_background_color(background_color_value)
+            elif 'plot_name_input.value' == triggered_id:
+                logging.info("'plot_name_input.value' triggered")
+                return self.evaluate_plot_name(plot_name_value)
+            elif 'plot_type_dropdown.value' == triggered_id:
+                logging.info("'plot_type_dropdown.value' triggered")
+                return self.evaluate_plot_type(plot_type_value)
+            elif 'database_dropdown.value' == triggered_id:
+                logging.info("'database_dropdown.value' triggered")
+                return self.evaluate_database(database_value)
+            elif 'constraint_checkboxes.value' == triggered_id:
+                logging.info("'constraint_checkboxes.value' triggered")
+                return self.evaluate_constraint_showr(constraint_showr_value)
+
+        # * download dataset
+        @app.callback(
+            Output(
+                component_id='download_dataset_dcc',
+                component_property='data'),
+            Input(
+                component_id='download_dataset_button',
+                component_property='n_clicks'),
+        )
+        def download_dataset(n_clicks):
+            """ Documentation incomplete. """
+
+            logging.info("'download_dataset_button.n_clicks' triggered")
+            return self.evaluate_dataset_download(n_clicks)
+
+        # * create object holding selected data
+        @app.callback(
+            Output(
+                component_id='selection',
+                component_property='data'),
+            Input(
+                component_id='parmoo_graph',
+                component_property='selectedData'),
+            Input(
+                component_id='parmoo_graph',
+                component_property='restyleData'),
+        )
+        def store_selection(
+            selectedData,
+            restyleData,
+        ):
+            """ Documentation incomplete. """
+
+            triggered_id = callback_context.triggered[0]['prop_id']
+            if 'parmoo_graph.selectedData' == triggered_id:
+                logging.info("'parmoo_graph.selectedData' triggered")
+                self.evaluate_selected_data(selectedData, 'selectedData')
+            elif 'parmoo_graph.restyleData' == triggered_id:
+                logging.info("'parmoo_graph.restyleData' triggered")
+                if self.plot_type == 'parallel':
+                    self.evaluate_selected_data(restyleData, 'restyleData')
+
+        # * download selection
+        @app.callback(
+            Output(
+                component_id='download_selection_dcc',
+                component_property='data'),
+            Input(
+                component_id='download_selection_button',
+                component_property='n_clicks'),
+        )
+        def download_selection(n_clicks):
+            """ Documentation incomplete. """
+
+            logging.info("'download_selection_button.n_clicks' triggered")
+            return self.evaluate_selection_download(n_clicks)
+
+        # * update image export format
+        @app.callback(
+            Output(
+                component_id='image_export_format_store',
+                component_property='data'),
+            Input(
+                component_id='image_export_format_dropdown',
+                component_property='value'),
+        )
+        def update_image_export_format(value):
+            """ Documentation incomplete. """
+
+            logging.info("'image_export_format_dropdown.value' triggered")
+            self.evaluate_image_export_format(value)
+
+        # * update data export format
+        @app.callback(
+            Output(
+                component_id='data_export_format_store',
+                component_property='data'),
+            Input(
+                component_id='data_export_format_dropdown',
+                component_property='value'),
+        )
+        def update_data_export_format(value):
+            """ Documentation incomplete. """
+
+            logging.info("'data_export_format_dropdown.value' triggered")
+            self.evaluate_data_export_format(value)
+
+        # * export image
+        @app.callback(
+            Output(
+                component_id='image',
+                component_property='data'),
+            Input(
+                component_id='download_image_button',
+                component_property='n_clicks'),
+        )
+        def download_image(n_clicks):
+            """ Documentation incomplete. """
+
+            logging.info("'download_image_button.n_clicks' triggered")
+            return self.evaluate_image_download(n_clicks)
+
+    # ! EXECUTION
+
+        logging.info('initialized dashboard')
+
+        # * pop_up
+        if pop_up:
+            if not environ.get("WERKZEUG_RUN_MAIN"):
+                open_new(port)
+
+        # * run application
+        logging.info('opening dashboard in browser. this might take a while')
+        if dev_mode:
+            app.run(
+                debug=True,
+                dev_tools_hot_reload=True,
+            )
+        elif not dev_mode:
+            app.run(
+                debug=False,
+                dev_tools_hot_reload=False,
+            )
+
+    # ! INITIALIZATION HELPERS
+
+    def generate_graph(self):
+        """ Documentation incomplete. """
+
+        if self.plot_type == 'scatter':
+            self.graph = generate_scatter(
+                moop=self.moop,
+                db=self.db,
+                points=self.points,
+            )
+        elif self.plot_type == 'parallel':
+            self.graph = generate_parallel(
+                moop=self.moop,
+                db=self.db,
+                points=self.points,
+            )
+        elif self.plot_type == 'radar':
+            self.graph = generate_radar(
+                moop=self.moop,
+                db=self.db,
+                points=self.points,
+            )
+        else:
+            warn("invalid plot_type")
+
+        self.graph = self.update_height()
+        self.graph = self.update_width()
+        self.graph = self.update_font()
+        self.graph = self.update_font_size()
+        self.graph = self.update_plot_name()
+        self.graph = self.update_background_color()
+
+        return self.graph
+
+    def configure(self):
+        """ Documentation incomplete. """
+
+        if self.height != 'auto' and self.width != 'auto':
+            self.config = {
+                'displaylogo': False,
+                'displayModeBar': True,
+                'toImageButtonOptions': {
+                    'format': self.screenshot,
+                    'filename': str(self.plot_name),
+                    'height': int(self.height),
+                    'width': int(self.width),
+                    'scale': 1
+                }
+            }
+        else:
+            self.config = {
+                'displaylogo': False,
+                'displayModeBar': True,
+                'toImageButtonOptions': {
+                    'format': self.screenshot,
+                    'filename': str(self.plot_name),
+                }
+            }
+
+        return self.config
+
+    # ! UPDATE HELPERS
+
+    # * functionality of select height input
+    def update_height(self):
+        """ Documentation incomplete. """
+
+        if self.height != 'auto':
+            self.graph.update_layout(
+                height=int(self.height))
+        return self.graph
+
+    # * functionality of select width input
+    def update_width(self):
+        """ Documentation incomplete. """
+
+        if self.width != 'auto':
+            self.graph.update_layout(
+                width=int(self.width))
+        return self.graph
+
+    # * functionality of select font input
+    def update_font(self):
+        """ Documentation incomplete. """
+
+        if self.font != 'auto':
+            self.graph.update_layout(
+                font=dict(
+                    family=self.font
+                )
+            )
+        return self.graph
+
+    # * functionality of select font size input
+    def update_font_size(self):
+        """ Documentation incomplete. """
+
+        if self.fontsize != 'auto':
+            self.graph.update_layout(
+                font=dict(
+                    size=int(self.fontsize)
+                )
+            )
+        return self.graph
+
+    # * functionality of plot name input
+    def update_plot_name(self):
+        """ Documentation incomplete. """
+
+        self.graph.update_layout(
+            title_text=self.plot_name
+        )
+        return self.graph
+
+    # * functionality of background color dropdown
+    def update_background_color(self):
+        """ Documentation incomplete. """
+
+        if self.background_color != 'auto':
+            if self.plot_type == 'scatter':
+                self.graph.update_layout(
+                    plot_bgcolor=self.background_color,
+                    paper_bgcolor=self.background_color,
+                )
+            elif self.plot_type == 'parallel':
+                self.graph.update_layout(
+                    paper_bgcolor=self.background_color,
+                )
+            elif self.plot_type == 'radar':
+                self.graph.update_polars(
+                    bgcolor=self.background_color,
+                )
+                self.graph.update_layout(
+                    paper_bgcolor=self.background_color,
+                )
+        return self.graph
+
+    # * functionality of plot type dropdown
+    def update_plot_type(self):
+        """ Documentation incomplete. """
+
+        return self.generate_graph()
+
+    # * functionality of database dropdown
+    def update_database(self):
+        """ Documentation incomplete. """
+
+        self.database = set_database(
+            moop=self.moop,
+            db=self.db,
+            points=self.points
+        )
+        if self.plot_name == 'Pareto Front':
+            self.plot_name = set_plot_name(db=self.db)
+        elif self.plot_name == 'Objective Data':
+            self.plot_name = set_plot_name(db=self.db)
+        return self.generate_graph()
+
+    # ! CALLBACK HELPERS
+
+    def evaluate_height(self, height_value):
+        """ Documentation incomplete. """
+
+        if height_value is not None:
+            self.height = height_value
+            return self.update_height()
+        else:
+            return self.graph
+
+    def evaluate_width(self, width_value):
+        """ Documentation incomplete. """
+
+        if width_value is not None:
+            self.width = width_value
+            return self.update_width()
+        else:
+            return self.graph
+
+    def evaluate_font(self, font_value):
+        """ Documentation incomplete. """
+
+        if font_value != '':
+            self.font = font_value
+            self.graph = self.update_font()
+            return self.graph
+        else:
+            return self.graph
+
+    def evaluate_font_size(self, font_size_value):
+        """ Documentation incomplete. """
+
+        self.fontsize = font_size_value
+        self.graph = self.update_font_size()
+        return self.graph
+
+    def evaluate_background_color(self, background_color_value):
+        """ Documentation incomplete. """
+
+        if background_color_value == 'Transparent':
+            self.background_color = 'rgb(0,0,0,0)'
+        else:
+            self.background_color = background_color_value
+        return self.update_background_color()
+
+    def evaluate_plot_name(self, plot_name_value):
+        """ Documentation incomplete. """
+
+        if plot_name_value != '':
+            self.plot_name = plot_name_value
+            self.graph = self.update_plot_name()
+            return self.graph
+        else:
+            return self.graph
+
+    def evaluate_plot_type(self, plot_type_value):
+        """ Documentation incomplete. """
+
+        if plot_type_value == 'Scatterplot':
+            self.plot_type = 'scatter'
+        elif plot_type_value == 'Parallel Coordinates plot':
+            self.plot_type = 'parallel'
+        elif plot_type_value == 'Radar plot':
+            self.plot_type = 'radar'
+        return self.update_plot_type()
+
+    def evaluate_database(self, database_value):
+        """ Documentation incomplete. """
+
+        if database_value == 'Pareto Front':
+            self.db = 'pf'
+        elif database_value == 'Objective Data':
+            self.db = 'obj'
+        return self.update_database()
+
+    def evaluate_dataset_download(self, n_clicks):
+        """ Documentation incomplete. """
+
+        if n_clicks is None:
+            raise exceptions.PreventUpdate
+        else:
+            self.database.index.name = 'index'
+            if self.data_export_format == 'CSV':
+                return dict(
+                    filename=str(self.plot_name) + ".csv",
+                    content=self.database.to_csv(),
+                )
+            else:
+                return dict(
+                    filename=str(self.plot_name) + ".json",
+                    content=self.database.to_json(),
+                )
+
+    def evaluate_selected_data(self, data, type):
+        """ Documentation incomplete. """
+
+        if data is None:
+            raise exceptions.PreventUpdate
+        else:
+            self.selection_indexes = []
+            if type == 'selectedData':
+                selectedData = data
+                pointskey = selectedData['points']
+                for index in range(len(pointskey)):
+                    level1 = pointskey[index]
+                    point_index = level1['pointIndex']
+                    self.selection_indexes.append(point_index)
+            elif type == 'restyleData':
+                self.update_constraint_range(data)
+                objectives = self.moop.getObjectiveType().names
+                for i, row in self.database.iterrows():
+                    row_selected = True
+                    for objective in objectives:
+                        if row_selected:
+                            row_obj_value = row[objective]
+                            location = objectives.index(objective)
+                            entry_dict = self.constraint_range[location]
+                            ranges = entry_dict[list(entry_dict.keys())[0]]
+                            if ranges is not None:
+                                if row_selected:
+                                    for range in ranges:
+                                        try:
+                                            for rang in range:
+                                                for ran in rang:
+                                                    pass
+                                            row_selected_yet = False
+                                            for rang in range:
+                                                if not row_selected_yet:
+                                                    if (row_obj_value >=
+                                                       rang[0] and
+                                                       row_obj_value <=
+                                                       rang[1]):
+                                                        row_selected_yet = True
+                                                    else:
+                                                        row_selected_yet =False
+                                            row_selected = row_selected_yet
+                                        except:
+                                            if (row_obj_value >= range[0] and
+                                               row_obj_value <= range[1]):
+                                                row_selected = True
+                                            else:
+                                                row_selected = False
+                    if row_selected:
+                        self.selection_indexes.append(i)
+
+    def set_constraint_range(self, restyleData):
+        """ Documentation incomplete. """
+
+        if restyleData is None:
+            objectives = self.moop.getObjectiveType().names
+            self.constraint_range = [None] * len(objectives)
+            count = 0
+            for objective in objectives:
+                self.constraint_range[count] = dict({objective: None})
+                count += 1
+            return self.constraint_range
+        else:
+            return self.update_constraint_range(self, restyleData)
+
+    def update_constraint_range(self, restyleData):
+        """ Documentation incomplete. """
+
+        key_list = restyleData[0]
+        for key in key_list:
+            location = int(key[11])
+            entry_dict = self.constraint_range[location]
+            entry_dict[list(entry_dict.keys())[0]] = key_list[key]
+        return self.constraint_range
+
+    def evaluate_selection_download(self, n_clicks):
+        """ Documentation incomplete. """
+
+        if n_clicks is None:
+            raise exceptions.PreventUpdate
+        else:
+            selection_db = self.database.iloc[:0, :].copy()
+            for i in self.selection_indexes:
+                selection_db = pd.concat(
+                    [selection_db,
+                        self.database.iloc[[i]]]
+                )
+                selection_db.index.name = 'index'
+            selection_db.drop_duplicates(inplace=True)
+            selection_db.sort_index(inplace=True)
+            if self.data_export_format == 'CSV':
+                return dict(
+                    filename="selected_data.csv",
+                    content=selection_db.to_csv(),
+                )
+            else:
+                return dict(
+                    filename="selected_data.json",
+                    content=selection_db.to_json(),
+                )
+
+    def evaluate_image_export_format(self, image_export_format_value):
+        """ Documentation incomplete. """
+
+        if image_export_format_value is not None:
+            self.image_export_format = image_export_format_value
+
+    def evaluate_data_export_format(self, data_export_format_value):
+        """ Documentation incomplete. """
+
+        if data_export_format_value is not None:
+            self.data_export_format = data_export_format_value
+
+    def evaluate_image_download(self, n_clicks):
+        """ Documentation incomplete. """
+
+        if n_clicks is None:
+            raise exceptions.PreventUpdate
+        else:
+            file_name = str(self.plot_name)
+            file_name += '.' + str(self.image_export_format)
+            if (str(self.image_export_format) == 'HTML' or
+               str(self.image_export_format) == 'html'):
+                con_tent = pio.write_html(
+                        fig=self.graph,
+                        file=str(file_name)
+                )
+            else:
+                con_tent = pio.write_image(
+                        fig=self.graph,
+                        file=str(file_name)
+                )
+
+            return dict(
+                filename=file_name,
+                content=con_tent,
+            )
+
+    def evaluate_customization_options(self, action, n_clicks):
+        """ Documentation incomplete. """
+
+        if n_clicks is None:
+            raise exceptions.PreventUpdate
+        else:
+            showr = dict()
+            hider = dict(display='none')
+            if action == 'show':
+                return hider, showr, showr, showr, showr, showr, showr, showr
+            else:
+                return showr, hider, hider, hider, hider, hider, hider, hider
+
+    def evaluate_export_options(self, action, n_clicks):
+        """ Documentation incomplete. """
+
+        if n_clicks is None:
+            return no_update, no_update
+        else:
+            showr = dict()
+            hider = dict(display='none')
+            if action == 'show':
+                return hider, showr, showr, showr
+            else:
+                return showr, hider, hider, hider
+
+    def evaluate_constraint_showr(self, value):
+        """ Documentation incomplete. """
+
+        """ Evaluate constraint toggles and update graph accordingly.
+
+        Args:
+            value (string): A string representing the state of the constraint
+                toggles:
+                 * 'constraint_satisfying' - 'Show constraint-satisfying
+                   points' is the only toggle selected. Update graph to show
+                   only points that satisfy every constraint.
+                 * 'constraint_violating' - 'Show constraint-violating points'
+                   is the only toggle selected. Update graph to show only
+                   points that violate any constraint.
+                 * 'all' - Both constraint toggles are selected. Update graph
+                   to include all points.
+                 * 'none' - No constraint toggles are selected. Update graph
+                   to include no points.
+
+        Returns:
+            (plotly.graph_objects.Figure): A graph containing the points
+            selected by the constraint toggles.
+
+        """
+
+        if value is None:
+            raise exceptions.PreventUpdate
+        else:
+            if value == ['constraint_satisfying']:
+                self.points = 'constraint_satisfying'
+            elif value == ['constraint_violating']:
+                self.points = 'constraint_violating'
+            elif value == ['constraint_satisfying', 'constraint_violating']:
+                self.points = 'all'
+            elif value == ['constraint_violating', 'constraint_satisfying']:
+                self.points = 'all'
+            else:
+                self.points = 'none'
+        self.database = set_database(
+            moop=self.moop,
+            db=self.db,
+            points=self.points,
+        )
+        return self.generate_graph()
diff --git a/parmoo/viz/graph.py b/parmoo/viz/graph.py
new file mode 100644
index 0000000..7486302
--- /dev/null
+++ b/parmoo/viz/graph.py
@@ -0,0 +1,229 @@
+""" This module contains tools to generate interactive Plotly-based graphs.
+
+The functions are:
+
+ Display data in interactive browser plot
+  * ``generate_scatter(moop)`` -- Generate interactive scatterplot matrix
+  * ``generate_parallel(moop)`` -- Generate interactive parallel plot
+  * ``generate_radar(moop)`` -- Generate interactive radar plot
+
+"""
+
+import plotly.express as px
+import plotly.graph_objects as go
+import logging
+from .utilities import (
+    set_plot_name,
+    set_database,
+    set_hover_info,
+)
+
+
+def generate_scatter(
+    moop,
+    db,
+    points,
+):
+
+    """ Generate a scatterplot or scatterplot matrix.
+
+        Args:
+            moop (MOOP): An object containing the data to be visualized.
+
+            db (string): Filter traces by dataset.
+             * 'pf' - Plot the Pareto Front.
+             * 'obj' - Plot objective data.
+
+            points (string): Filter traces by constraint score.
+             * 'constraint_satisfying' - Show only points that
+               satisfy every constraint.
+             * 'constraint_violating' - Show only points that
+               violate any constraint.
+             * 'all' - Plot all points.
+             * 'none' - Plot no points.
+
+        Returns:
+            plotly.graph_objects.Figure: A scatterplot or scatterplot matrix
+            displaying traces that fit the filtering criteria.
+    """
+
+    # * intro log
+    logging.info('generating scatterplot. this might take a while')
+
+    # * get info
+    objectives = moop.getObjectiveType().names
+
+    # * choose database
+    database = set_database(moop, db=db, points=points)
+    plot_name = set_plot_name(db=db)
+
+    # * create plot
+    if (len(objectives) == 2):
+        fig = px.scatter(
+            database,
+            x=objectives[0],
+            y=objectives[1],
+            title=plot_name,
+            hover_data=database.columns,
+            template='none',
+        )
+        # fig.update_xaxes(showticklabels=False)
+        # fig.update_yaxes(showticklabels=False)
+    else:
+        fig = px.scatter_matrix(
+            database,
+            dimensions=objectives,
+            title=plot_name,
+            hover_data=database.columns,
+            template='none',
+        )
+        fig.update_traces(diagonal_visible=False)
+
+    # * logging outro
+    logging.info('generated scatterplot')
+
+    # * return figure
+    return fig
+
+
+def generate_parallel(
+    moop,
+    db,
+    points,
+):
+
+    """ Generate a parallel coordinates plot.
+
+        Args:
+            moop (MOOP): An object containing the data to be visualized.
+
+            db (string): Filter traces by dataset.
+             * 'pf' - Plot the Pareto Front.
+             * 'obj' - Plot objective data.
+
+            points (string): Filter traces by constraint score.
+             * 'constraint_satisfying' - Show only points that
+               satisfy every constraint.
+             * 'constraint_violating' - Show only points that
+               violate any constraint.
+             * 'all' - Plot all points.
+             * 'none' - Plot no points.
+
+        Returns:
+            plotly.graph_objects.Figure: A parallel coordinates plot
+            displaying traces that fit the filtering criteria.
+
+    """
+
+    # * intro log
+    message = 'generating parallel coordinates plot. '
+    message += 'this might take a while'
+    logging.info(message)
+
+    # * setup axes
+    objectives = moop.getObjectiveType().names
+
+    # * choose database
+    database = set_database(moop, db=db, points=points)
+    plot_name = set_plot_name(db=db)
+
+    # * create plot
+    fig = px.parallel_coordinates(
+        database,
+        dimensions=objectives,
+        title=plot_name,
+        template='none',
+    )
+
+    # * logging outro
+    logging.info('generated parallel coordinates plot')
+
+    # * return figure
+    return fig
+
+
+def generate_radar(
+    moop,
+    db,
+    points,
+):
+
+    """ Generate a radar plot.
+
+        Args:
+            moop (MOOP): An object containing the data to be visualized.
+
+            db (string): Filter traces by dataset.
+             * 'pf' - Plot the Pareto Front.
+             * 'obj' - Plot objective data.
+
+            points (string): Filter traces by constraint score.
+             * 'constraint_satisfying' - Show only points that
+               satisfy every constraint.
+             * 'constraint_violating' - Show only points that
+               violate any constraint.
+             * 'all' - Plot all points.
+             * 'none' - Plot no points.
+
+        Returns:
+            plotly.graph_objects.Figure: A radar plot
+            displaying traces that fit the filtering criteria.
+
+    """
+
+    # * intro log
+    logging.info('generating radar plot. this might take awhile')
+
+    # * setup axes
+    objectives = moop.getObjectiveType().names
+    wrap = objectives[0]
+    temp_variable = list(objectives)
+    temp_variable.append(wrap)
+    axes = tuple(temp_variable)
+
+    # * choose database
+    database = set_database(moop, db=db, points=points)
+    plot_name = set_plot_name(db=db)
+
+    # * create scaled database
+    j = database.copy(deep=True)
+    for i in j.columns:
+        j[i] = (j[i] - j[i].min()) / (j[i].max() - j[i].min())
+    scaled_db = j
+
+    # * create plot
+    fig = go.Figure()
+    for i in range(len(database)):
+        trace = (i)
+        hover_info = set_hover_info(database=database, i=i,)
+        values = []
+        for key in axes:
+            values.append(scaled_db[key][i])
+        fig.add_trace(go.Scatterpolar(
+            r=values,
+            theta=axes,
+            name=trace,
+            hovertext=hover_info,))
+
+    # * improve aesthetics
+    fig.update_layout(
+        template='none',
+    )
+    fig.update_layout(
+        polar=dict(
+            radialaxis=dict(
+                visible=True,
+                showticklabels=False,)))
+    fig.update_layout(
+        title=dict(
+            text=plot_name))
+    fig.update_layout(
+        autosize=True,)
+    fig.update_layout(
+        showlegend=False)
+
+    # * logging outro
+    logging.info('generated radar plot')
+
+    # * return figure
+    return fig
diff --git a/parmoo/viz/plot.py b/parmoo/viz/plot.py
new file mode 100644
index 0000000..5f61dd0
--- /dev/null
+++ b/parmoo/viz/plot.py
@@ -0,0 +1,475 @@
+
+""" A plotting library for interactive visualization of MOOP objects.
+
+To generate a plot, generate create a MOOP object and use it to solve a problem.
+Then call one of the following function, passing the MOOP object as the
+first argument:
+
+ * ``viz.scatter(moop)``
+ * ``viz.parallel_coordinates(moop)``
+ * ``viz.radar(moop)``
+
+Please take note of the following:
+
+ * Via the interactivity features, all of the arguments except for the
+   ``moop`` field can be adjusted interactively through the pop-up GUI.
+   The plot type can also be adjusted from the dropdown menu while running
+   the GUI..
+ * The viz tool hosts a Dash app through the local port
+   ``http://127.0.0.1:8050/``.
+   This means that only one visualization can be hosted at a time.
+   Also, killing the Dash app will end the interactivity in the browser,
+   although a static plot may remain in your broswer window.
+ * Finally, note that there is a known issue when using the Chrome browser.
+   The Firefox or Safari browsers are recommended.
+
+To interact with the plots:
+
+ 1. To multi-select in scatterplots and radar plots, hold down ``SHIFT``
+    while making additional selections.
+ 2. To remove all selections from a scatterplot and radar plot, select without
+    holding down the ``SHIFT`` key or double click.
+ 3. To select in parallel coordinates plots, click along an axis and then
+    drag the cursor elsewhere along the same axis.
+    The highlighted part of the axis is the area that is selected.
+    There is no need to hold down SHIFT when making multiple selections in
+    parallel coordinates plots.
+ 4. To remove a parallel coordinates plot selection, click on the highlighted
+    selection bar you wish to delete.
+ 5. To reset plot interactions made through the toolbar in the top right of a
+    graph (not interactions made through buttons, dropdowns, toggles, or input
+    boxes), double click on the plot. This will undo selections, zoom, pan,
+    etc.
+
+The three basic plot options are detailed below.
+
+"""
+
+import logging
+from .dashboard import (
+    Dash_App
+)
+from .utilities import (
+    export_file,
+    set_plot_name,
+    check_inputs,
+)
+from .graph import (
+    generate_scatter,
+    generate_parallel,
+    generate_radar,
+)
+
+
+def scatter(moop,
+            db='pf',
+            output='dash',
+            points='constraint_satisfying',
+            height='auto',
+            width='auto',
+            font='auto',
+            fontsize='auto',
+            background_color='auto',
+            screenshot='svg',
+            image_export_format='svg',
+            data_export_format='csv',
+            dev_mode=False,
+            pop_up=True,
+            port='http://127.0.0.1:8050/',
+           ):
+    """ Create a scatter plot matrix to visualize the results of a MOOP.
+
+    Args:
+        moop (MOOP): The MOOP results that you would like to visualize.
+
+        db (str): Either 'pf' to plot just the Pareto front, or 'obj'
+            to plot the complete objective database. Defaults to 'pf'.
+
+        output (str): Either 'dash' to generate an interactive plot
+            running in your browser using the dash app, or anything
+            else to save a static plot to the desktop. Defaults to
+            'dash'.
+
+        points (str): Plot only
+            constraint satisfying points ('constraint_satisfying'),
+            constraint violating points ('constraint_violating'),
+            all points ('all'),
+            or no points ('none').
+
+        height (str): The height in pixels of the resulting figure.
+            Defaults to 'auto', which matches your screen size.
+
+        width (str): The width in pixels of the resulting figure.
+            Defaults to 'auto', which matches your screen size.
+
+        font (str): The font that will be used for axis labels and legends.
+            These values are automatically inferred from the name fields
+            of your MOOP object. Any specified font must be available on your
+            computer and available in the appropriate path. Defaults 'auto',
+            which is times new roman on most machines.
+
+        fontsize (str): The font size (in points). Defaults to 'auto', which
+            infers the size based on the plot dimensions.
+
+        background_color (str): Set the background color for this plot.
+            Defaults to 'auto', which is white with grey axis lines on
+            most systems.
+
+        screenshot (str): Set the download mode when saving a screenshot
+            using the "screenshot" button. Defaults to 'svg'.
+            Other available options include:
+            'html', 'webp', 'jpeg', 'png', 'svg', 'eps', and 'pdf'.
+            Note that the 'eps' option requires the poppler library, which
+            is not included in any of ParMOO's dependency lists.
+
+        image_export_format (str): Set the export format when exporting
+            a plot directly image file. Defaults to 'svg'.
+            Other available options include:
+            'html', 'webp', 'jpeg', 'png', 'svg', 'eps', and 'pdf'.
+            Note that the 'eps' option requires the poppler library, which
+            is not included in any of ParMOO's dependency lists.
+
+        data_export_format (str): Set the format for exporting selected data
+            to a file. Defaults to 'csv'. The other option is 'json'.
+
+        dev_mode (bool): Run in developer mode, which allows changes to the
+            code to automatically render in the browser. Activating this
+            mode will interfere with some functionalities (such as
+            checkpointing) since it results in multiple calls to the script.
+            This value defaults to False, and should only be adjusted by
+            developers.
+
+        pop_up (bool): Automatically pop-up the dash app when called.
+            Defaults to True. The only reason one might want to adjust, is
+            if the environment prevents pop-ups or a non default browser
+            is desired.
+
+        port (str): The port through which the Dash app is hosted.
+            Defaults to 'http://127.0.0.1:8050/'.
+
+    """
+
+    check_inputs(
+        db=db,
+        output=output,
+        points=points,
+        height=height,
+        width=width,
+        font=font,
+        fontsize=fontsize,
+        background_color=background_color,
+        screenshot=screenshot,
+        image_export_format=image_export_format,
+        data_export_format=data_export_format,
+        dev_mode=dev_mode,
+        pop_up=pop_up,
+        port=port,
+    )
+
+    logging.info('initialized scatter() wrapper')
+
+    # * output
+    if output == 'dash':
+        Dash_App(
+            plot_type='scatter',
+            moop=moop,
+            db=db,
+            points=points,
+            height=height,
+            width=width,
+            font=font,
+            fontsize=fontsize,
+            background_color=background_color,
+            screenshot=screenshot,
+            image_export_format=image_export_format,
+            data_export_format=data_export_format,
+            dev_mode=dev_mode,
+            pop_up=pop_up,
+            port=port,
+        )
+    else:
+        plot_name = set_plot_name(db=db)
+        export_file(
+            fig=generate_scatter(
+                moop,
+                db=db,
+                points=points,
+            ),
+            plot_name=plot_name,
+            file_type=output,
+        )
+
+
+def parallel_coordinates(moop,
+                         db='pf',
+                         output='dash',
+                         points='constraint_satisfying',
+                         height='auto',
+                         width='auto',
+                         font='auto',
+                         fontsize='auto',
+                         background_color='auto',
+                         screenshot='svg',
+                         image_export_format='svg',
+                         data_export_format='csv',
+                         dev_mode=False,
+                         pop_up=True,
+                         port='http://127.0.0.1:8050/',
+                        ):
+    """ Create a parallel coordinates plot to visualize the results of a MOOP.
+
+    Args:
+        moop (MOOP): The MOOP results that you would like to visualize.
+
+        db (str): Either 'pf' to plot just the Pareto front, or 'obj'
+            to plot the complete objective database. Defaults to 'pf'.
+
+        output (str): Either 'dash' to generate an interactive plot
+            running in your browser using the dash app, or anything
+            else to save a static plot to the desktop. Defaults to
+            'dash'.
+
+        points (str): Plot only
+            constraint satisfying points ('constraint_satisfying'),
+            constraint violating points ('constraint_violating'),
+            all points ('all'),
+            or no points ('none').
+
+        height (str): The height in pixels of the resulting figure.
+            Defaults to 'auto', which matches your screen size.
+
+        width (str): The width in pixels of the resulting figure.
+            Defaults to 'auto', which matches your screen size.
+
+        font (str): The font that will be used for axis labels and legends.
+            These values are automatically inferred from the name fields
+            of your MOOP object. Any specified font must be available on your
+            computer and available in the appropriate path. Defaults 'auto',
+            which is times new roman on most machines.
+
+        fontsize (str): The font size (in points). Defaults to 'auto', which
+            infers the size based on the plot dimensions.
+
+        background_color (str): Set the background color for this plot.
+            Defaults to 'auto', which is white with grey axis lines on
+            most systems.
+
+        screenshot (str): Set the download mode when saving a screenshot
+            using the "screenshot" button. Defaults to 'svg'.
+            Other available options include:
+            'html', 'webp', 'jpeg', 'png', 'svg', 'eps', and 'pdf'.
+            Note that the 'eps' option requires the poppler library, which
+            is not included in any of ParMOO's dependency lists.
+
+        image_export_format (str): Set the export format when exporting
+            a plot directly image file. Defaults to 'svg'.
+            Other available options include:
+            'html', 'webp', 'jpeg', 'png', 'svg', 'eps', and 'pdf'.
+            Note that the 'eps' option requires the poppler library, which
+            is not included in any of ParMOO's dependency lists.
+
+        data_export_format (str): Set the format for exporting selected data
+            to a file. Defaults to 'csv'. The other option is 'json'.
+
+        dev_mode (bool): Run in developer mode, which allows changes to the
+            code to automatically render in the browser. Activating this
+            mode will interfere with some functionalities (such as
+            checkpointing) since it results in multiple calls to the script.
+            This value defaults to False, and should only be adjusted by
+            developers.
+
+        pop_up (bool): Automatically pop-up the dash app when called.
+            Defaults to True. The only reason one might want to adjust, is
+            if the environment prevents pop-ups or a non default browser
+            is desired.
+
+        port (str): The port through which the Dash app is hosted.
+            Defaults to 'http://127.0.0.1:8050/'.
+
+    """
+
+    check_inputs(
+        db=db,
+        output=output,
+        points=points,
+        height=height,
+        width=width,
+        font=font,
+        fontsize=fontsize,
+        background_color=background_color,
+        screenshot=screenshot,
+        image_export_format=image_export_format,
+        data_export_format=data_export_format,
+        dev_mode=dev_mode,
+        pop_up=pop_up,
+        port=port,
+    )
+
+    logging.info('initialized parallel_coordinates() wrapper')
+
+    if output == 'dash':
+        Dash_App(
+            plot_type='parallel',
+            moop=moop,
+            db=db,
+            points=points,
+            height=height,
+            width=width,
+            font=font,
+            fontsize=fontsize,
+            background_color=background_color,
+            screenshot=screenshot,
+            image_export_format=image_export_format,
+            data_export_format=data_export_format,
+            dev_mode=dev_mode,
+            pop_up=pop_up,
+            port=port,
+        )
+    else:
+        plot_name = set_plot_name(db=db)
+        export_file(
+            fig=generate_parallel(
+                moop=moop,
+                db=db,
+                points=points,
+            ),
+            plot_name=plot_name,
+            file_type=output
+        )
+
+
+def radar(
+          moop,
+          db='pf',
+          output='dash',
+          points='constraint_satisfying',
+          height='auto',
+          width='auto',
+          font='auto',
+          fontsize='auto',
+          background_color='auto',
+          screenshot='svg',
+          image_export_format='svg',
+          data_export_format='csv',
+          dev_mode=False,
+          pop_up=True,
+          port='http://127.0.0.1:8050/',
+         ):
+    """ Create a radar plot to visualize the results of a MOOP.
+
+    Args:
+        moop (MOOP): The MOOP results that you would like to visualize.
+
+        db (str): Either 'pf' to plot just the Pareto front, or 'obj'
+            to plot the complete objective database. Defaults to 'pf'.
+
+        output (str): Either 'dash' to generate an interactive plot
+            running in your browser using the dash app, or anything
+            else to save a static plot to the desktop. Defaults to
+            'dash'.
+
+        points (str): Plot only
+            constraint satisfying points ('constraint_satisfying'),
+            constraint violating points ('constraint_violating'),
+            all points ('all'),
+            or no points ('none').
+
+        height (str): The height in pixels of the resulting figure.
+            Defaults to 'auto', which matches your screen size.
+
+        width (str): The width in pixels of the resulting figure.
+            Defaults to 'auto', which matches your screen size.
+
+        font (str): The font that will be used for axis labels and legends.
+            These values are automatically inferred from the name fields
+            of your MOOP object. Any specified font must be available on your
+            computer and available in the appropriate path. Defaults 'auto',
+            which is times new roman on most machines.
+
+        fontsize (str): The font size (in points). Defaults to 'auto', which
+            infers the size based on the plot dimensions.
+
+        background_color (str): Set the background color for this plot.
+            Defaults to 'auto', which is white with grey axis lines on
+            most systems.
+
+        screenshot (str): Set the download mode when saving a screenshot
+            using the "screenshot" button. Defaults to 'svg'.
+            Other available options include:
+            'html', 'webp', 'jpeg', 'png', 'svg', 'eps', and 'pdf'.
+            Note that the 'eps' option requires the poppler library, which
+            is not included in any of ParMOO's dependency lists.
+
+        image_export_format (str): Set the export format when exporting
+            a plot directly image file. Defaults to 'svg'.
+            Other available options include:
+            'html', 'webp', 'jpeg', 'png', 'svg', 'eps', and 'pdf'.
+            Note that the 'eps' option requires the poppler library, which
+            is not included in any of ParMOO's dependency lists.
+
+        data_export_format (str): Set the format for exporting selected data
+            to a file. Defaults to 'csv'. The other option is 'json'.
+
+        dev_mode (bool): Run in developer mode, which allows changes to the
+            code to automatically render in the browser. Activating this
+            mode will interfere with some functionalities (such as
+            checkpointing) since it results in multiple calls to the script.
+            This value defaults to False, and should only be adjusted by
+            developers.
+
+        pop_up (bool): Automatically pop-up the dash app when called.
+            Defaults to True. The only reason one might want to adjust, is
+            if the environment prevents pop-ups or a non default browser
+            is desired.
+
+        port (str): The port through which the Dash app is hosted.
+            Defaults to 'http://127.0.0.1:8050/'.
+
+    """
+
+    check_inputs(
+        db=db,
+        output=output,
+        points=points,
+        height=height,
+        width=width,
+        font=font,
+        fontsize=fontsize,
+        background_color=background_color,
+        screenshot=screenshot,
+        image_export_format=image_export_format,
+        data_export_format=data_export_format,
+        dev_mode=dev_mode,
+        pop_up=pop_up,
+        port=port,
+    )
+    logging.info('initialized radar() wrapper')
+    if output == 'dash':
+        Dash_App(
+            plot_type='radar',
+            moop=moop,
+            db=db,
+            points=points,
+            height=height,
+            width=width,
+            font=font,
+            fontsize=fontsize,
+            background_color=background_color,
+            screenshot=screenshot,
+            image_export_format=image_export_format,
+            data_export_format=data_export_format,
+            dev_mode=dev_mode,
+            pop_up=pop_up,
+            port=port,
+        )
+    else:
+        plot_name = set_plot_name(db=db)
+        export_file(
+            fig=generate_radar(
+                moop,
+                db=db,
+                points=points,
+            ),
+            plot_name=plot_name,
+            file_type=output
+        )
diff --git a/parmoo/viz/utilities.py b/parmoo/viz/utilities.py
new file mode 100644
index 0000000..3a06dce
--- /dev/null
+++ b/parmoo/viz/utilities.py
@@ -0,0 +1,415 @@
+""" This module contains utilities (helper functions) that are used throughout
+the viz tool.
+
+"""
+
+import pandas as pd
+import logging
+
+def export_file(fig, plot_name, file_type):
+
+    """ Export image of figure to working directory.
+
+        Args:
+            fig (plotly.graph_objects.Figure): The figure to export.
+
+            plot_name (string): Set the filename of the image file.
+
+            file_type (string): Set the image file type.
+             - 'html' - Export as .html file.
+             - 'pdf' - Export as .pdf file.
+             - 'svg' - Export as .svg file.
+             - 'eps' - Export as .eps file
+               if the poppler dependency is installed.
+             - 'jpeg' - Export as .jpeg file.
+             - 'png' - Export as .png file.
+             - 'webp' - Export as .webp file.
+
+    """
+
+    if file_type == 'html':
+        fig.write_html(plot_name + ".html")
+        logging.info("exported graph as .html")
+    elif file_type == 'pdf':
+        fig.write_image(plot_name + ".pdf")
+        logging.info("exported graph as .pdf")
+    elif file_type == 'svg':
+        fig.write_image(plot_name + ".svg")
+        logging.info("exported graph as .svg")
+    elif file_type == 'eps':
+        fig.write_image(plot_name + ".eps")
+        logging.info("exported graph as .eps")
+    elif file_type == 'jpeg':
+        fig.write_image(plot_name + ".jpeg")
+        logging.info("exported graph as .jpeg")
+    elif file_type == 'png':
+        fig.write_image(plot_name + ".png")
+        logging.info("exported graph as .png")
+    elif file_type == 'webp':
+        fig.write_image(plot_name + ".webp")
+        logging.info("exported graph as .webp")
+
+
+def set_plot_name(db):
+
+    """ Provide a default graph title.
+
+        Args:
+            db (string): Graph contents inform title.
+             - 'pf' - Set plot name to "Pareto Front"
+             - 'obj' - Set plot name to "Objective Data"
+
+        Returns:
+            plot_name (string): The default plot name.
+
+    """
+
+    if db == 'pf':
+        plot_name = "Pareto Front"
+    elif db == 'obj':
+        plot_name = "Objective Data"
+    return plot_name
+
+
+def set_database(moop, db, points):
+
+    """ Choose which points from MOOP object to plot.
+
+        Args:
+            db (string): Set dataset.
+             - 'pf' - Set Pareto Front as dataset.
+             - 'obj' - Set objective data as dataset.
+
+            points (string): Filter traces from dataset by constraint score.
+             - 'constraint_satisfying' - Include only points that
+               satisfy every constraint.
+             - 'constraint_violating' - Include only points that
+               violate any constraint.
+             - 'all' - Include all points in dataset.
+             - 'none' - Include no points in dataset.
+
+        Returns:
+            df (Pandas dataframe): A 2D dataframe containing post-filter
+            data from the MOOP.
+
+    """
+
+    if db == 'pf':
+        database = pd.DataFrame(moop.getPF())
+    elif db == 'obj':
+        database = pd.DataFrame(moop.getObjectiveData())
+    if moop.getConstraintType() is None:
+        df = database
+    else:
+        if points == 'constraint_satisfying':
+            constraints = moop.getConstraintType().names
+            df = database.copy(deep=True)
+            for constraint in constraints:
+                indices = df[df[constraint] > 0].index
+                df.drop(indices, inplace=True)
+                df.reset_index(inplace=True)
+        elif points == 'constraint_violating':
+            constraints = moop.getConstraintType().names
+            df = database.copy(deep=True)
+            for constraint in constraints:
+                indices = df[df[constraint] <= 0].index
+                df.drop(indices, inplace=True)
+                df.reset_index(inplace=True)
+        elif points == 'all':
+            df = database
+        elif points == 'none':
+            df = database[0:0]
+    return df
+
+
+def set_hover_info(database, i):
+
+    """ Customize information in hover label for trace i.
+
+        Args:
+            database (Pandas dataframe): A 2D dataframe containing the
+                traces to be graphed.
+
+            i (int): An index indicating the row where the trace
+                we're labeling is located.
+
+        Returns:
+            hover_info (string): An HTML-format string to display when
+            users hover over trace i.
+
+    """
+
+    hover_info = ""
+    for key in database.columns:
+        hover_info += str(key)
+        hover_info += ": "
+        hover_info += str(database[key][i])
+        hover_info += "<br>"
+        # since plotly is JavaScript-based
+        # it uses HTML string formatting
+    return hover_info
+
+
+def check_inputs(
+    db,
+    output,
+    points,
+    height,
+    width,
+    font,
+    fontsize,
+    background_color,
+    screenshot,
+    image_export_format,
+    data_export_format,
+    dev_mode,
+    pop_up,
+    port,
+):
+
+    """ Check keyword inputs to user-facing functions for validity
+
+        Args:
+            db: The item passed to the 'db' keyword in a user-facing function.
+                If db cannot be cast to a string valued 'pf' or 'obj',
+                a ValueError is raised.
+
+            output: The item passed to the 'output' keyword in a
+                user-facing function.
+                If output cannot be cast to a string corresponding to one of
+                the supported output filetypes, a ValueError is raised.
+
+            points: The item passed to the 'points' keyword in a
+                user-facing function.
+                If points cannot be cast to a string corresponding to one of
+                the supported contraint filters, a ValueError is raised.
+
+            height: The item passed to the 'height' keyword in a user-facing
+                function.
+                If height is not the default string 'auto' or cannot be cast
+                to an int
+                of value greater than one, a ValueError is raised.
+
+            width: The item passed to the 'width' keyword in a user-facing
+                function.
+                If width is not the default string 'auto' or cannot be cast
+                to an int
+                of value greater than one, a ValueError is raised.
+
+            font: The item passed to the 'font' keyword in a user-facing
+                function.
+                If font cannot be cast to a string, a ValueError is raised.
+
+            fontsize: The item passed to the 'fontsize' keyword in a
+                user-facing function.
+                If fontsize is not the default value 'auto' or cannot be cast
+                to an int
+                of value between 1 and 100 inclusive, a ValueError is raised.
+
+            background_color: The item passed to the 'background_color'
+                keyword in a user-facing function.
+                If background_color cannot be cast to a string, a ValueError
+                is raised.
+
+            screenshot: The item passed to the 'screenshot' keyword in a
+                user-facing function.
+                If screenshot cannot be cast to a string corresponding to one
+                of the supported
+                screenshot filetypes, a ValueError is raised.
+
+            image_export_format: The item passed to the 'image_export_format'
+                keyword in a user-facing function.
+                If image_export_format cannot be cast to a string
+                corresponding to one of the supported
+                image_export_format filetypes, a ValueError is raised.
+
+            data_export_format: The item passed to the 'data_export_format'
+                keyword in a user-facing function.
+                If data_export_format cannot be cast to a string corresponding
+                to one of the supported
+                data_export_format filetypes, a ValueError is raised.
+
+            data_export_format: The item passed to the 'data_export_format'
+                keyword in a user-facing function.
+                If data_export_format cannot be cast to a string corresponding
+                to one of the supported
+                data_export_format filetypes, a ValueError is raised.
+
+            dev_mode: The item passed to the 'dev_mode' keyword in a
+                user-facing function.
+                If dev_mode cannot be cast to one of the boolean values True
+                and False, a ValueError is raised.
+
+            pop_up: The item passed to the 'pop_up' keyword in a user-facing
+                function.
+                If pop_up cannot be cast to one of the boolean values True and
+                False, a ValueError is raised.
+
+            port: The item passed to the 'port' keyword in a user-facing
+                function.
+                If port cannot be cast to a string beginning with 'http', a
+                ValueError is raised.
+
+        Raises:
+            A ValueError if any of the values passed by a user to a keyword in
+            a user-facing function are judged invalid.
+
+    """
+
+    try:
+        if (str(db) == 'pf' or
+            str(db) == 'obj'):
+            pass
+        else:
+            raise ValueError(str(db) + " is an invalid value for 'db'")
+    except:
+        raise ValueError(str(db) + " is an invalid value for 'db'")
+
+    try:
+        if (str(output) == 'dash' or
+            str(output) == 'html' or
+            str(output) == 'svg' or
+            str(output) == 'pdf' or
+            str(output) == 'eps' or
+            str(output) == 'jpeg' or
+            str(output) == 'png' or
+            str(output) == 'webp'):
+            pass
+        else:
+            raise ValueError(str(output) + " is an invalid value for 'output'")
+    except:
+        raise ValueError(str(output) + " is an invalid value for 'output'")
+
+    try:
+        if (str(points) == 'constraint_satisfying' or
+            str(points) == 'constraint_violating' or
+            str(points) == 'all' or
+            str(points) == 'none'):
+            pass
+        else:
+            raise ValueError(str(points) + " is an invalid value for 'points'")
+    except:
+        raise ValueError(str(points) + " is an invalid value for 'points'")
+
+    if (height == 'auto' or
+        int(height) >= 1):
+        pass
+    else:
+        raise ValueError(str(height) + " is an invalid value for 'height'")
+
+    try:
+        if (width == 'auto' or
+            int(width) >= 1):
+            pass
+        else:
+            raise ValueError(str(width) + " is an invalid value for 'width'")
+    except:
+        raise ValueError(str(width) + " is an invalid value for 'width'")
+
+    try:
+        if (str(type(str(font))) == "<class 'str'>"):
+            pass
+        else:
+            raise ValueError(str(font) + " is an invalid value for 'font'")
+    except:
+        raise ValueError(str(font) + " is an invalid value for 'font'")
+
+    try:
+        if (fontsize == 'auto' or
+           (int(fontsize) >= 1 and
+            int(fontsize) <= 100)):
+            pass
+        else:
+            message = str(fontsize)
+            message += " is an invalid value for 'fontsize'"
+            raise ValueError(message)
+    except:
+        message = str(fontsize)
+        message += " is an invalid value for 'fontsize'"
+        raise ValueError(message)
+
+    try:
+        if (str(type(str(background_color))) == "<class 'str'>"):
+            pass
+        else:
+            message = str(background_color)
+            message += " is an invalid value for 'background_color'"
+            raise ValueError(message)
+    except:
+        message = str(background_color)
+        message += " is an invalid value for 'background_color'"
+        raise ValueError(message)
+
+    try:
+        if (screenshot == 'png' or
+            screenshot == 'svg' or
+            screenshot == 'jpeg' or
+            screenshot == 'webp'):
+            pass
+        else:
+            message = str(screenshot)
+            message += " is an invalid value for 'screenshot'"
+            raise ValueError(message)
+    except:
+        message = str(screenshot)
+        message += " is an invalid value for 'screenshot'"
+        raise ValueError(message)
+
+    try:
+        if (image_export_format == 'html' or
+            image_export_format == 'svg' or
+            image_export_format == 'pdf' or
+            image_export_format == 'eps' or
+            image_export_format == 'jpeg' or
+            image_export_format == 'png' or
+            image_export_format == 'webp'):
+            pass
+        else:
+            message = str(image_export_format)
+            message += " is an invalid value for 'image_export_format'"
+            raise ValueError(message)
+    except:
+        message = str(image_export_format)
+        message += " is an invalid value for 'image_export_format'"
+        raise ValueError(message)
+
+    try:
+        if (data_export_format == 'json' or
+            data_export_format == 'csv'):
+            pass
+        else:
+            message = str(data_export_format)
+            message += " is an invalid value for 'data_export_format'"
+            raise ValueError(message)
+    except:
+        message = str(data_export_format)
+        message += " is an invalid value for 'data_export_format'"
+        raise ValueError(message)
+
+    try:
+        if (dev_mode or not dev_mode):
+            pass
+        else:
+            message = str(dev_mode)
+            message += " is an invalid value for 'dev_mode'"
+            raise ValueError(message)
+    except:
+        message = str(dev_mode)
+        message += " is an invalid value for 'dev_mode'"
+        raise ValueError(message)
+
+    try:
+        if (pop_up or not pop_up):
+            pass
+        else:
+            raise ValueError(str(pop_up) + " is an invalid value for 'pop_up'")
+    except:
+        raise ValueError(str(pop_up) + " is an invalid value for 'pop_up'")
+
+    try:
+        if (port[0:4] == 'http'):
+            pass
+        else:
+            raise ValueError(str(port) + " is an invalid value for 'port'")
+    except:
+        raise ValueError(str(port) + " is an invalid value for 'port'")
diff --git a/setup.py b/setup.py
index a4b5b96..583bf6d 100644
--- a/setup.py
+++ b/setup.py
@@ -62,6 +62,7 @@ def run_tests(self):
     packages=["parmoo",
               "parmoo.acquisitions",
               "parmoo.extras",
+              "parmoo.viz",
               "parmoo.optimizers",
               "parmoo.searches",
               "parmoo.surrogates",
@@ -73,13 +74,13 @@ def run_tests(self):
               "parmoo.tests.libe_tests",
               "parmoo.tests.regression_tests"],
 
-    install_requires=["numpy", "scipy", "pyDOE"],
+    install_requires=["numpy", "scipy", "pyDOE", "pandas"],
 
     # If run tests through setup.py - downloads these but does not install
     tests_require=["pytest", "pytest-cov", "flake8"],
 
     extras_require={
-        'extras': ["libensemble"],
+        'extras': ["libensemble", "plotly", "dash", "kaleido"],
         'docs': ["sphinx", "sphinxcontrib.bibtex", "sphinx_rtd_theme"]},
 
     classifiers=[


Test patch:
diff --git a/parmoo/tests/libe_tests/libe_funcs.py b/parmoo/tests/libe_tests/libe_funcs.py
new file mode 100644
index 0000000..89a97ad
--- /dev/null
+++ b/parmoo/tests/libe_tests/libe_funcs.py
@@ -0,0 +1,53 @@
+# Define a 5d problem with 3 objectives
+n = 5
+o = 3
+
+def dtlz2_sim_unnamed(x):
+    """ Evaluates the sim function for a collection of points given in
+    ``H['x']``.
+
+    """
+
+    import math
+    import numpy as np
+
+
+    # Create output array for sim outs
+    f = np.zeros(o)
+    # Compute the kernel function g(x)
+    gx = np.dot(x[o-1:n]-0.5, x[o-1:n]-0.5)
+    # Compute the simulation outputs
+    f[0] = (1.0 + gx)
+    for y in x[:o-1]:
+        f[0] *= math.cos(math.pi * y / 2.0)
+    for i in range(1, o):
+        f[i] = (1.0 + gx) * math.sin(math.pi * x[o-1-i] / 2.0)
+        for y in x[:o-1-i]:
+            f[i] *= math.cos(math.pi * y / 2.0)
+    return f
+    
+def obj1_unnamed(x, s): return s[0]
+def obj2_unnamed(x, s): return s[1]
+def obj3_unnamed(x, s): return s[2]
+
+# Define functions for named runs
+
+def dtlz2_sim_named(x):
+    """ Evaluates the sim function for a collection of points given in
+    ``H['x']``.
+
+    """
+
+    import numpy as np
+
+    # Unpack names into array
+    xx = np.zeros(n)
+    names = [f"x{i+1}" for i in range(n)]
+    for i, name in enumerate(names):
+        xx[i] = x[name]
+    # Use dtlz2_sim to evaluate
+    return dtlz2_sim_unnamed(xx)
+
+def obj1_named(x, s): return s['DTLZ2'][0]
+def obj2_named(x, s): return s['DTLZ2'][1]
+def obj3_named(x, s): return s['DTLZ2'][2]
diff --git a/parmoo/tests/libe_tests/test_libe_gen.py b/parmoo/tests/libe_tests/test_libe_gen.py
index 233fc91..20d23c0 100644
--- a/parmoo/tests/libe_tests/test_libe_gen.py
+++ b/parmoo/tests/libe_tests/test_libe_gen.py
@@ -24,105 +24,61 @@
 from parmoo.surrogates import GaussRBF
 from parmoo.searches import LatinHypercube
 from parmoo.acquisitions import RandomConstraint
-
-# Solve a 5d problem with 3 objectives
-n = 5
-o = 3
-
-def dtlz2_sim(x):
-    """ Evaluates the sim function for a collection of points given in
-    ``H['x']``.
-
-    """
-
-    import math
-
-    # Create output array for sim outs
-    f = np.zeros(o)
-    # Compute the kernel function g(x)
-    gx = np.dot(x[o-1:n]-0.5, x[o-1:n]-0.5)
-    # Compute the simulation outputs
-    f[0] = (1.0 + gx)
-    for y in x[:o-1]:
-        f[0] *= math.cos(math.pi * y / 2.0)
-    for i in range(1, o):
-        f[i] = (1.0 + gx) * math.sin(math.pi * x[o-1-i] / 2.0)
-        for y in x[:o-1-i]:
-            f[i] *= math.cos(math.pi * y / 2.0)
-    return f
-
-# Create a libE_MOOP
-moop = libE_MOOP(LocalGPS, hyperparams={})
-# Add n design vars
-for i in range(n):
-    moop.addDesign({'lb': 0.0, 'ub': 1.0})
-# Add simulation
-moop.addSimulation({'m': o,
-                    'sim_func': dtlz2_sim,
-                    'hyperparams': {'search_budget': 100},
-                    'search': LatinHypercube,
-                    'surrogate': GaussRBF,
-                    'sim_db': {},
-                    'des_tol': 0.00000001})
-# Add o objectives
-def obj1(x, s): return s[0]
-def obj2(x, s): return s[1]
-def obj3(x, s): return s[2]
-moop.addObjective({'obj_func': obj1})
-moop.addObjective({'obj_func': obj2})
-moop.addObjective({'obj_func': obj3})
-# Add 4 acquisition functions
-for i in range(4):
-    moop.addAcquisition({'acquisition': RandomConstraint})
-
-# Solve
-moop.solve()
-assert(moop.getObjectiveData()['x_vals'].shape[0] == 200)
-
-#print(moop.getPF())
-
-# Create a libE_MOOP with named variables
-moop = libE_MOOP(LocalGPS, hyperparams={})
-# Add n design vars
-for i in range(n):
-    moop.addDesign({'name': "x" + str(i + 1), 'lb': 0.0, 'ub': 1.0})
-
-def dtlz2_sim_named(x):
-    """ Evaluates the sim function for a collection of points given in
-    ``H['x']``.
-
-    """
-
-    # Unpack names into array
-    xx = np.zeros(n)
-    for i, name in enumerate(moop.moop.des_names):
-        xx[i] = x[name[0]]
-    # Use dtlz2_sim to evaluate
-    return dtlz2_sim(xx)
-
-# Add simulation
-moop.addSimulation({'name': "DTLZ2",
-                    'm': o,
-                    'sim_func': dtlz2_sim_named,
-                    'hyperparams': {'search_budget': 100},
-                    'search': LatinHypercube,
-                    'surrogate': GaussRBF,
-                    'sim_db': {},
-                    'des_tol': 0.00000001})
-# Add o objectives
-def obj1(x, s): return s['DTLZ2'][0]
-def obj2(x, s): return s['DTLZ2'][1]
-def obj3(x, s): return s['DTLZ2'][2]
-moop.addObjective({'name': "obj1", 'obj_func': obj1})
-moop.addObjective({'name': "obj2", 'obj_func': obj2})
-moop.addObjective({'name': "obj3", 'obj_func': obj3})
-# Add 4 acquisition functions
-for i in range(4):
-    moop.addAcquisition({'acquisition': RandomConstraint})
-
-# Solve
-moop.solve()
-assert(moop.getObjectiveData()['x1'].shape[0] == 200)
-
-#print(moop.getPF())
-#print(moop.getSimulationData())
+from parmoo.tests.libe_tests.libe_funcs import *
+
+# On MacOS and Windows, libE runs using Python MP must be
+# enclosed in an "if __name__ == '__main__':" block, as below
+if __name__ == "__main__":
+    # Create a libE_MOOP
+    moop = libE_MOOP(LocalGPS, hyperparams={})
+    # Add n design vars
+    for i in range(n):
+        moop.addDesign({'lb': 0.0, 'ub': 1.0})
+    # Add simulation
+    moop.addSimulation({'m': o,
+                        'sim_func': dtlz2_sim_unnamed,
+                        'hyperparams': {'search_budget': 100},
+                        'search': LatinHypercube,
+                        'surrogate': GaussRBF,
+                        'sim_db': {},
+                        'des_tol': 0.00000001})
+    # Add o objectives
+    moop.addObjective({'obj_func': obj1_unnamed})
+    moop.addObjective({'obj_func': obj2_unnamed})
+    moop.addObjective({'obj_func': obj3_unnamed})
+    # Add 4 acquisition functions
+    for i in range(4):
+        moop.addAcquisition({'acquisition': RandomConstraint})
+    
+    # Solve
+    moop.solve()
+    assert(moop.getObjectiveData()['x_vals'].shape[0] == 200)
+    
+    #print(moop.getPF())
+    
+    # Create a libE_MOOP with named variables
+    moop = libE_MOOP(LocalGPS, hyperparams={})
+    # Add n design vars
+    for i in range(n):
+        moop.addDesign({'name': "x" + str(i + 1), 'lb': 0.0, 'ub': 1.0})
+    
+    # Add simulation
+    moop.addSimulation({'name': "DTLZ2",
+                        'm': o,
+                        'sim_func': dtlz2_sim_named,
+                        'hyperparams': {'search_budget': 100},
+                        'search': LatinHypercube,
+                        'surrogate': GaussRBF,
+                        'sim_db': {},
+                        'des_tol': 0.00000001})
+    # Add o objectives
+    moop.addObjective({'name': "obj1", 'obj_func': obj1_named})
+    moop.addObjective({'name': "obj2", 'obj_func': obj2_named})
+    moop.addObjective({'name': "obj3", 'obj_func': obj3_named})
+    # Add 4 acquisition functions
+    for i in range(4):
+        moop.addAcquisition({'acquisition': RandomConstraint})
+    
+    # Solve
+    moop.solve()
+    assert(moop.getObjectiveData()['x1'].shape[0] == 200)
diff --git a/parmoo/tests/regression_tests/test_infeasible_prob.py b/parmoo/tests/regression_tests/test_infeasible_prob.py
new file mode 100644
index 0000000..a1717b5
--- /dev/null
+++ b/parmoo/tests/regression_tests/test_infeasible_prob.py
@@ -0,0 +1,75 @@
+
+""" Use ParMOO to "solve" an infeasible problem.
+
+Ensure that ParMOO correctly identifies the most nearly feasible point.
+
+"""
+
+from parmoo import MOOP
+from parmoo.optimizers import LBFGSB
+from parmoo.surrogates import GaussRBF
+from parmoo.acquisitions import UniformWeights
+from parmoo.searches import LatinHypercube
+import os
+import numpy as np
+
+# For this test, use all user-defined functions
+
+def sim(x):
+    " User sim for sample problem. "
+    return [sum([x[name] for name in x.dtype.names])]
+
+def obj(x, sx, der=0):
+    " User objective for sample problem. "
+    if der == 1:
+        return np.zeros(1, x.dtype)[0]
+    elif der == 2:
+        dsx = np.zeros(1, sx.dtype)[0]
+        dsx["Sample sim"] = 3.0
+        return dsx
+    else:
+        return 3.0 * sx["Sample sim"]
+
+def const(x, sx, der=0):
+    " User constraint for sample problem. "
+    if der == 1:
+        dx = np.ones(1, x.dtype)[0]
+        for name in x.dtype.names:
+            dx[name] *= -2.0
+        return dx
+    elif der == 2:
+        return np.zeros(1, sx.dtype)[0]
+    else:
+        return sum([(x[name] - 1.0) ** 2 for name in x.dtype.names])
+
+# Create a MOOP
+moop = MOOP(LBFGSB)
+
+# Add 2 continuous design variables
+for i in range(2):
+    moop.addDesign({'name': f"x{i+1}", 'ub': 1.0, 'lb': 0.0,
+                    'des_type': "continuous", 'des_tol': 1.0e-8})
+
+# Add the simulation
+moop.addSimulation({'name': "Sample sim",
+                    'm': 1,
+                    'sim_func': sim,
+                    'search': LatinHypercube,
+                    'surrogate': GaussRBF,
+                    'hyperparams': {}})
+
+# Add user objective functions
+moop.addObjective({'name': "My objective", 'obj_func': obj})
+
+# Add user constraint functions, with only 1 feasible point
+moop.addConstraint({'name': "My constraint", 'constraint': const})
+
+# Add 1 acquisition function
+moop.addAcquisition({'acquisition': UniformWeights, 'hyperparams': {}})
+
+# Solve the problem with 5 iterations to search for feasible point
+moop.solve(5)
+
+# Check that all solutions are (nearly) feasible
+assert(all([np.all(np.abs(moop.getPF()[name] - 1.0 < 1.0e-8))
+            for name in moop.getDesignType().names]))
diff --git a/parmoo/tests/regression_tests/test_mixed_variables.py b/parmoo/tests/regression_tests/test_mixed_variables.py
new file mode 100644
index 0000000..1685c29
--- /dev/null
+++ b/parmoo/tests/regression_tests/test_mixed_variables.py
@@ -0,0 +1,79 @@
+
+""" Use ParMOO to solve a convex, user-defined problem, with mixed variables
+(continuous, integer, categorical, and custom).
+
+Uses named variables and public function definitions to define the problem.
+
+"""
+
+from parmoo import MOOP
+from parmoo.optimizers import GlobalGPS
+from parmoo.surrogates import GaussRBF
+from parmoo.acquisitions import RandomConstraint
+from parmoo.searches import LatinHypercube
+import os
+import numpy as np
+
+# For this test, use all user-defined functions
+
+def sim(x):
+    " User sim for sample problem. "
+    # Categorical variables
+    if x["cat var 1"] == "good":
+        result = 0.0
+    else:
+        result = 1.0
+    if x["cat var 2"] != "low":
+        result += 1.0
+    return np.array([result + x["cont var"] ** 2 + x["int var"] ** 2 +
+                     float(x["custom var"]) ** 2,
+                     result + (x["cont var"] - 1.0) ** 2 + x["int var"] ** 2 +
+                     float(x["custom var"]) ** 2])
+
+def obj1(x, sx):
+    " User obj1 for sample problem. "
+    return sx["my sim"][0]
+
+def obj2(x, sx):
+    " User obj2 for sample problem. "
+    return sx["my sim"][1]
+
+# Create a MOOP
+moop = MOOP(GlobalGPS)
+
+# Add design variables
+moop.addDesign({'name': "cont var", 'ub': 1.0, 'lb': 0.0,
+                'des_type': "continuous", 'des_tol': 1.0e-8})
+moop.addDesign({'name': "int var", 'ub': 5, 'lb': -5,
+                'des_type': "integer"})
+moop.addDesign({'name': "cat var 1", 'des_type': "categorical",
+                'levels': ["good", "bad"]})
+moop.addDesign({'name': "cat var 2", 'des_type': "categorical",
+                'levels': ["low", "med", "high"]})
+moop.addDesign({'name': "custom var", 'des_type': "custom",
+                'embedding_size': 1,
+                'embedder': lambda x: float(x),
+                'extracter': lambda x: str(x)})
+
+# Add the simulation
+moop.addSimulation({'name': "my sim",
+                    'm': 2,
+                    'sim_func': sim,
+                    'search': LatinHypercube,
+                    'surrogate': GaussRBF,
+                    'hyperparams': {'search_budget': 20}})
+
+# Add user objective functions
+moop.addObjective({'obj_func': obj1}, {'obj_func': obj2})
+
+# Add NUM_OBJ acquisition funcitons
+for i in range(1):
+    moop.addAcquisition({'acquisition': RandomConstraint, 'hyperparams': {}})
+
+# Solve the problem with 5 iterations with checkpointing on
+moop.solve(20)
+
+# Check that 40 simulations were evaluated and solutions are feasible
+assert(moop.getSimulationData()["my sim"].size == 40)
+assert(moop.getObjectiveData().size == 40)
+assert(moop.getPF().size > 0)
diff --git a/parmoo/tests/unit_tests/test_epsilon_constraint.py b/parmoo/tests/unit_tests/test_epsilon_constraint.py
index fdf5732..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), {})
@@ -49,23 +49,24 @@ def obj_f(x):
         acqu.setTarget(data, lambda x: np.ones(1), {})
     data['c_vals'] = np.zeros((10, 1))
     # Set a few good target
-    assert(np.all(acqu.setTarget({}, lambda x: np.zeros(1), {}) < 1.0))
-    assert(np.all(acqu.setTarget({}, lambda x: np.zeros(1), {}) > 0.0))
+    assert(np.all(acqu.setTarget({}, lambda x: np.zeros(3), {}) < 1.0))
+    assert(np.all(acqu.setTarget({}, lambda x: np.zeros(3), {}) > 0.0))
     assert(np.all(acqu.setTarget({'x_vals': np.zeros((1, 3)),
                                   'f_vals': np.zeros((1, 3)),
                                   'c_vals': np.zeros((1, 1))},
-                                 lambda x: 0.01 - sum(x), {}) < 1.0))
-    assert(acqu.setTarget(data, lambda x: np.zeros(1), {}) in data['x_vals'])
+                                 lambda x: np.ones(3) * (0.01 - sum(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))
     # Generate a random scalarization target and check the scalarization
     acqu = RandomConstraint(3, np.zeros(3), np.ones(3), {})
     acqu.setTarget({'x_vals': None, 'f_vals': None},
-                   lambda x: np.zeros(1), {})
-    acqu.setTarget(data, lambda x: np.zeros(0), {})
+                   lambda x: np.zeros(3), {})
+    acqu.setTarget(data, lambda x: np.zeros(3), {})
     # Get a copy of the Pareto front for checking correctness
     pf = updatePF(data, {})
     # Check that the scalar value is either less than the sum of fi or bad
@@ -73,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 576324e..c57d02c 100644
--- a/parmoo/tests/unit_tests/test_gps_search.py
+++ b/parmoo/tests/unit_tests/test_gps_search.py
@@ -35,19 +35,19 @@ def L(z): return np.ones(2)
     def g(z): return np.ones((2, 3))
     # Create 2 acquisition functions targeting 2 "pure" solutions
     acqu1 = UniformWeights(o, lb, ub, {})
-    acqu1.setTarget({}, lambda x: np.zeros(1), {})
+    acqu1.setTarget({}, lambda x: np.zeros(2), {})
     acqu1.weights[:] = 0.0
     acqu1.weights[0] = 1.0
     acqu2 = UniformWeights(o, lb, ub, {})
-    acqu2.setTarget({}, lambda x: np.zeros(1), {})
+    acqu2.setTarget({}, lambda x: np.zeros(2), {})
     acqu2.weights[:] = 0.0
     acqu2.weights[1] = 1.0
     # Create a third acquisition function targeting a random tradeoff solution
     acqu3 = UniformWeights(o, lb, ub, {})
-    acqu3.setTarget({}, lambda x: np.zeros(1), {})
+    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,24 +55,24 @@ 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)
     opt.setConstraints(lambda z: np.asarray([0.1 - z[2], z[2] - 0.6]))
-    opt.setLagrangian(L, g)
+    opt.setPenalty(L, g)
     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)))
@@ -134,23 +134,23 @@ def L(z): return np.ones(2)
     def g(z): return np.ones((2, 3))
     # Create 2 acquisition functions targeting 2 "pure" solutions
     acqu1 = UniformWeights(o, lb, ub, {})
-    acqu1.setTarget({}, lambda x: np.zeros(1), {})
+    acqu1.setTarget({}, lambda x: np.zeros(2), {})
     acqu1.weights[:] = 0.0
     acqu1.weights[0] = 1.0
     acqu2 = UniformWeights(o, lb, ub, {})
-    acqu2.setTarget({}, lambda x: np.zeros(1), {})
+    acqu2.setTarget({}, lambda x: np.zeros(2), {})
     acqu2.weights[:] = 0.0
     acqu2.weights[1] = 1.0
     # Create a third acquisition function targeting a random tradeoff solution
     acqu3 = UniformWeights(o, lb, ub, {})
-    acqu3.setTarget({}, lambda x: np.zeros(1), {})
+    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,24 +164,24 @@ 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)
     opt.setConstraints(lambda z: np.asarray([0.1 - z[2], z[2] - 0.6]))
-    opt.setLagrangian(L, g)
+    opt.setPenalty(L, g)
     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 d156407..3836207 100644
--- a/parmoo/tests/unit_tests/test_lbfgsb.py
+++ b/parmoo/tests/unit_tests/test_lbfgsb.py
@@ -30,7 +30,7 @@ def test_LBFGSB():
     lb = np.zeros(n)
     ub = np.ones(n)
 
-    # Create the biobjective function and its augmented Lagrangian
+    # Create the biobjective function and its penalty function
     def f(z):
         return np.asarray([-z[0] + z[1] + z[2], z[0] - z[1] + z[2]])
 
@@ -52,19 +52,19 @@ def g(z):
 
     # Create 2 acquisition functions targeting 2 "pure" solutions
     acqu1 = UniformWeights(o, lb, ub, {})
-    acqu1.setTarget({}, lambda x: np.zeros(1), {})
+    acqu1.setTarget({}, lambda x: np.zeros(2), {})
     acqu1.weights[:] = 0.0
     acqu1.weights[0] = 1.0
     acqu2 = UniformWeights(o, lb, ub, {})
-    acqu2.setTarget({}, lambda x: np.zeros(1), {})
+    acqu2.setTarget({}, lambda x: np.zeros(2), {})
     acqu2.weights[:] = 0.0
     acqu2.weights[1] = 1.0
     # Create a third acquisition function targeting a random tradeoff solution
     acqu3 = UniformWeights(o, lb, ub, {})
-    acqu3.setTarget({}, lambda x: np.zeros(1), {})
+    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,32 +72,32 @@ 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(TypeError):
+        opt.setPenalty(5, lambda z: np.zeros(1))
     with pytest.raises(ValueError):
-        opt.setLagrangian(5, lambda z: np.zeros(1))
-    with pytest.raises(ValueError):
-        opt.setLagrangian(lambda z1, z2: np.zeros(1), lambda z: np.zeros(1))
-    with pytest.raises(ValueError):
-        opt.setLagrangian(lambda z: np.zeros(1), 5)
-    with pytest.raises(ValueError):
-        opt.setLagrangian(lambda z: np.zeros(1), lambda z1, z2: np.zeros(1))
+        opt.setPenalty(lambda z1, z2: np.zeros(1), lambda z: np.zeros(1))
+    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(TypeError):
         opt.addAcquisition(5)
     # Add the correct objective and constraints
     opt.setObjective(f)
     opt.setConstraints(lambda z: np.asarray([0.1 - z[2], z[2] - 0.6]))
-    opt.setLagrangian(L, g)
+    opt.setPenalty(L, g)
     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)))
@@ -154,7 +154,7 @@ def test_TR_LBFGSB():
     lb = np.zeros(n)
     ub = np.ones(n)
 
-    # Create the biobjective function and its augmented Lagrangian
+    # Create the biobjective function and its penalty function
     def f(z):
         return np.asarray([-z[0] + z[1] + z[2], z[0] - z[1] + z[2]])
 
@@ -176,19 +176,19 @@ def g(z):
 
     # Create 2 acquisition functions targeting 2 "pure" solutions
     acqu1 = UniformWeights(o, lb, ub, {})
-    acqu1.setTarget({}, lambda x: np.zeros(1), {})
+    acqu1.setTarget({}, lambda x: np.zeros(2), {})
     acqu1.weights[:] = 0.0
     acqu1.weights[0] = 1.0
     acqu2 = UniformWeights(o, lb, ub, {})
-    acqu2.setTarget({}, lambda x: np.zeros(1), {})
+    acqu2.setTarget({}, lambda x: np.zeros(2), {})
     acqu2.weights[:] = 0.0
     acqu2.weights[1] = 1.0
     # Create a third acquisition function targeting a random tradeoff solution
     acqu3 = UniformWeights(o, lb, ub, {})
-    acqu3.setTarget({}, lambda x: np.zeros(1), {})
+    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,36 +196,36 @@ 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(TypeError):
+        opt.setPenalty(5, lambda z: np.zeros(1))
     with pytest.raises(ValueError):
-        opt.setLagrangian(5, lambda z: np.zeros(1))
-    with pytest.raises(ValueError):
-        opt.setLagrangian(lambda z1, z2: np.zeros(1), lambda z: np.zeros(1))
-    with pytest.raises(ValueError):
-        opt.setLagrangian(lambda z: np.zeros(1), 5)
-    with pytest.raises(ValueError):
-        opt.setLagrangian(lambda z: np.zeros(1), lambda z1, z2: np.zeros(1))
+        opt.setPenalty(lambda z1, z2: np.zeros(1), lambda z: np.zeros(1))
+    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(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)
     # Add the correct objective and constraints
     opt.setObjective(f)
     opt.setConstraints(lambda z: np.asarray([0.1 - z[2], z[2] - 0.6]))
-    opt.setLagrangian(L, g)
+    opt.setPenalty(L, g)
     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 70db27a..aac75ea 100644
--- a/parmoo/tests/unit_tests/test_moop.py
+++ b/parmoo/tests/unit_tests/test_moop.py
@@ -32,445 +32,6 @@ def test_MOOP_init():
     assert(moop.hyperparams['test'] == 0)
 
 
-def test_MOOP_addDesign_bad_cont():
-    """ Check that the MOOP class handles adding bad continuous variables.
-
-    Initialize a MOOP objects, and add several bad continuous design
-    variables.
-
-    """
-
-    from parmoo import MOOP
-    from parmoo.optimizers import LocalGPS
-    import pytest
-
-    # Initialize a MOOP with no hyperparameters
-    moop = MOOP(LocalGPS)
-    # Try to add some bad design variable types
-    with pytest.raises(TypeError):
-        moop.addDesign([])
-    with pytest.raises(TypeError):
-        moop.addDesign({'des_type': 1.0})
-    with pytest.raises(ValueError):
-        moop.addDesign({'des_type': "hello world"})
-    # Add some bad continuous variables
-    with pytest.raises(AttributeError):
-        moop.addDesign({'des_type': "continuous"})
-    with pytest.raises(TypeError):
-        moop.addDesign({'des_type': "continuous",
-                        'des_tol': "hello world",
-                        'lb': 0.0,
-                        'ub': 1.0})
-    with pytest.raises(ValueError):
-        moop.addDesign({'des_type': "continuous",
-                        'des_tol': -1.0,
-                        'lb': 0.0,
-                        'ub': 1.0})
-    with pytest.raises(TypeError):
-        moop.addDesign({'des_type': "continuous",
-                        'lb': "hello",
-                        'ub': "world"})
-    with pytest.raises(ValueError):
-        moop.addDesign({'des_type': "continuous",
-                        'lb': 0.0,
-                        'ub': 0.0})
-    with pytest.raises(TypeError):
-        moop.addDesign({'name': 5,
-                        'des_type': "continuous",
-                        'lb': 0.0,
-                        'ub': 1.0})
-    # Add some bad continuous variables, using default option
-    with pytest.raises(AttributeError):
-        moop.addDesign({})
-    with pytest.raises(TypeError):
-        moop.addDesign({'des_tol': "hello world",
-                        'lb': 0.0,
-                        'ub': 1.0})
-    with pytest.raises(ValueError):
-        moop.addDesign({'des_tol': -1.0,
-                        'lb': 0.0,
-                        'ub': 1.0})
-    with pytest.raises(TypeError):
-        moop.addDesign({'lb': "hello",
-                        'ub': "world"})
-    with pytest.raises(ValueError):
-        moop.addDesign({'lb': 0.0,
-                        'ub': 0.0})
-    with pytest.raises(TypeError):
-        moop.addDesign({'name': 5,
-                        'lb': 0.0,
-                        'ub': 1.0})
-    # Try to use a repeated name to test error handling
-    with pytest.raises(ValueError):
-        moop.addDesign({'name': "x_1", 'lb': 0.0, 'ub': 1.0})
-        moop.addDesign({'name': "x_1",
-                        'des_type': "continuous",
-                        'lb': 0.0,
-                        'ub': 1.0})
-    # Add variables out of order
-    with pytest.raises(RuntimeError):
-        moop1 = MOOP(LocalGPS)
-        moop1.acquisitions.append(0)
-        moop1.addDesign({'des_type': "continuous",
-                         'lb': 0.0,
-                         'ub': 1.0})
-    with pytest.raises(RuntimeError):
-        moop2 = MOOP(LocalGPS)
-        moop2.sim_funcs.append(0)
-        moop2.addDesign({'des_type': "continuous",
-                         'lb': 0.0,
-                         'ub': 1.0})
-
-
-def test_MOOP_addDesign_bad_cat():
-    """ Check that the MOOP class handles adding bad categorical variables.
-
-    Initialize a MOOP objects, and add several bad categorical design
-    variables.
-
-    """
-
-    from parmoo import MOOP
-    from parmoo.optimizers import LocalGPS
-    import pytest
-
-    # Initialize a MOOP with no hyperparameters
-    moop = MOOP(LocalGPS)
-    # Add some bad categorical variables
-    with pytest.raises(AttributeError):
-        moop.addDesign({'des_type': "categorical"})
-    with pytest.raises(TypeError):
-        moop.addDesign({'des_type': "categorical",
-                        'levels': 1.0})
-    with pytest.raises(ValueError):
-        moop.addDesign({'des_type': "categorical",
-                        'levels': 1})
-    with pytest.raises(TypeError):
-        moop.addDesign({'name': 5,
-                        'des_type': "categorical",
-                        'levels': 2})
-    with pytest.raises(TypeError):
-        moop.addDesign({'des_type': "categorical",
-                        'levels': [3, "hi"]})
-    with pytest.raises(ValueError):
-        moop.addDesign({'des_type': "categorical",
-                        'levels': ["hi"]})
-
-
-def test_MOOP_addDesign():
-    """ Check that the MOOP class handles adding design variables properly.
-
-    Initialize a MOOP objects, and add several design variables.
-
-    """
-
-    from parmoo import MOOP
-    from parmoo.optimizers import LocalGPS
-
-    # Initialize a MOOP with no hyperparameters
-    moop = MOOP(LocalGPS)
-    # Now add some continuous design variables
-    assert(moop.n == 0)
-    moop.addDesign({'des_type': "continuous",
-                    'lb': 0.0,
-                    'ub': 1.0})
-    assert(moop.n == 1)
-    moop.addDesign({'des_type': "continuous",
-                    'des_tol': 0.01,
-                    'lb': 0.0,
-                    'ub': 1.0})
-    assert(moop.n == 2)
-    moop.addDesign({'name': "x_3",
-                    'des_type': "continuous",
-                    'des_tol': 0.01,
-                    'lb': 0.0,
-                    'ub': 1.0})
-    assert(moop.n == 3)
-    moop.addDesign({'lb': 0.0,
-                    'ub': 1.0})
-    assert(moop.n == 4)
-    moop.addDesign({'des_tol': 0.01,
-                    'lb': 0.0,
-                    'ub': 1.0})
-    assert(moop.n == 5)
-    moop.addDesign({'name': "x_6",
-                    'des_tol': 0.01,
-                    'lb': 0.0,
-                    'ub': 1.0})
-    assert(moop.n == 6)
-    # Now add some good categorical design variables
-    assert(moop.n_cat == 0)
-    moop.addDesign({'des_type': "categorical",
-                    'levels': 2})
-    assert(moop.n_cat == 1)
-    moop.addDesign({'des_type': "categorical",
-                    'levels': 3})
-    assert(moop.n_cat == 2)
-    moop.addDesign({'name': "x_9",
-                    'des_type': "categorical",
-                    'levels': 3})
-    assert(moop.n_cat == 3)
-    moop.addDesign({'name': "x_10",
-                    'des_type': "categorical",
-                    'levels': ["boy", "girl", "doggo"]})
-    assert(moop.n_cat == 4)
-    # Now add more continuous design variables
-    moop.addDesign({'des_type': "continuous",
-                    'lb': 0.0,
-                    'ub': 1.0})
-    assert(moop.n_cont == 7)
-    moop.addDesign({'lb': 0.0,
-                    'ub': 1.0})
-    assert(moop.n_cont == 8)
-
-
-def test_MOOP_embed_extract_unnamed1():
-    """ Test that the MOOP class can embed/extract unnamed design variables.
-
-    Add several design variables and generate an embedding. Then embed and
-    extract several inputs, and check that the results match up to the
-    design tolerance. This test applies to the three hidden methods:
-     * MOOP.__embed__(x)
-     * MOOP.__extract__(x)
-     * MOOP.__generate_encoding__()
-
-    """
-
-    from parmoo import MOOP
-    from parmoo.optimizers import LocalGPS
-    import numpy as np
-
-    # Initialize a MOOP with no hyperparameters
-    moop = MOOP(LocalGPS)
-    # Add two continuous variables and check that they are embedded correctly
-    moop.addDesign({'lb': 0.0,
-                    'ub': 1000.0})
-    moop.addDesign({'lb': -1.0,
-                    'ub': 0.0})
-    # Test 5 random variables
-    for i in range(5):
-        xi = np.random.random_sample(2)
-        xi[0] *= 1000.0
-        xi[1] -= 1.0
-        xxi = moop.__embed__(xi)
-        # Check that embedding is legal
-        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
-        assert(xxi.size == moop.n)
-        # Check extraction
-        assert(all(moop.__extract__(xxi) - xi < 1.0e-8))
-    # Test upper and lower bounds
-    x0 = np.zeros(2)
-    x0[0] *= 1000.0
-    x0[1] -= 1.0
-    xx0 = moop.__embed__(x0)
-    # Check that embedding is legal
-    assert(all(xx0 >= 0.0) and all(xx0 <= 1.0))
-    assert(xx0.size == moop.n)
-    # Check extraction
-    assert(all(moop.__extract__(xx0) - x0 < 1.0e-8))
-    x1 = np.ones(2)
-    x1[0] *= 1000.0
-    x1[1] -= 1.0
-    xx1 = moop.__embed__(x1)
-    # Check that embedding is legal
-    assert(all(xx1 >= 0.0) and all(xx1 <= 1.0))
-    assert(xx1.size == moop.n)
-    # Check extraction
-    assert(all(moop.__extract__(xx1) - x1 < 1.0e-8))
-    # Add two categorical variables and check that they are embedded correctly
-    moop.addDesign({'des_type': "categorical",
-                    'levels': 2})
-    moop.addDesign({'des_type': "categorical",
-                    'levels': 3})
-    # Test 5 random variables
-    for i in range(5):
-        xi = np.random.random_sample(4)
-        xi[0] *= 1000.0
-        xi[1] -= 1.0
-        xi[2:] = np.round(xi[2:])
-        xxi = moop.__embed__(xi)
-        # Check that embedding is legal
-        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
-        assert(xxi.size == moop.n)
-        # Check extraction
-        assert(all(moop.__extract__(xxi) - xi < 1.0e-8))
-    # Test upper and lower bounds
-    x0 = np.zeros(4)
-    x0[0] *= 1000.0
-    x0[1] -= 1.0
-    x0[2:] = np.round(x0[2:])
-    xx0 = moop.__embed__(x0)
-    # Check that embedding is legal
-    assert(all(xx0 >= 0.0) and all(xx0 <= 1.0))
-    assert(xx0.size == moop.n)
-    # Check extraction
-    assert(all(moop.__extract__(xx0) - x0 < 1.0e-8))
-    x1 = np.ones(4)
-    x1[0] *= 1000.0
-    x1[1] -= 1.0
-    x1[2:] = np.round(x1[2:])
-    xx1 = moop.__embed__(x1)
-    # Check that embedding is legal
-    assert(all(xx1 >= 0.0) and all(xx1 <= 1.0))
-    assert(xx1.size == moop.n)
-    # Check extraction
-    assert(all(moop.__extract__(xx1) - x1 < 1.0e-8))
-
-
-def test_MOOP_embed_extract_unnamed2():
-    """ Test that the MOOP class can embed/extract unnamed design variables.
-
-    Add several design variables and generate an embedding. Then embed and
-    extract several inputs, and check that the results match up to the
-    design tolerance. This test applies to the three hidden methods:
-     * MOOP.__embed__(x)
-     * MOOP.__extract__(x)
-     * MOOP.__generate_encoding__()
-
-    """
-
-    from parmoo import MOOP
-    from parmoo.optimizers import LocalGPS
-    import numpy as np
-
-    # Same problem as in test_MOOP_embed_extract_unnamed1(), but reverse order
-    moop = MOOP(LocalGPS)
-    # Add two categorical variables and check that they are embedded correctly
-    moop.addDesign({'des_type': "categorical",
-                    'levels': 2})
-    moop.addDesign({'des_type': "categorical",
-                    'levels': 3})
-    # Test 5 random variables
-    for i in range(5):
-        xi = np.random.random_sample(2)
-        xi = np.round(xi)
-        xxi = moop.__embed__(xi)
-        # Check that embedding is legal
-        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
-        assert(xxi.size == moop.n)
-        # Check extraction
-        assert(all(moop.__extract__(xxi) - xi < 1.0e-8))
-    # Test upper and lower bounds
-    x0 = np.zeros(2)
-    x0 = np.round(x0)
-    xx0 = moop.__embed__(x0)
-    # Check that embedding is legal
-    assert(all(xx0 >= 0.0) and all(xx0 <= 1.0))
-    assert(xx0.size == moop.n)
-    # Check extraction
-    assert(all(moop.__extract__(xx0) - x0 < 1.0e-8))
-    x1 = np.ones(2)
-    x1 = np.round(x1)
-    xx1 = moop.__embed__(x1)
-    # Check that embedding is legal
-    assert(all(xx1 >= 0.0) and all(xx1 <= 1.0))
-    assert(xx1.size == moop.n)
-    # Check extraction
-    assert(all(moop.__extract__(xx1) - x1 < 1.0e-8))
-    # Add two continuous variables and check that they are embedded correctly
-    moop.addDesign({'lb': 0.0,
-                    'ub': 1000.0})
-    moop.addDesign({'lb': -1.0,
-                    'ub': 0.0})
-    # Test 5 random variables
-    for i in range(5):
-        xi = np.random.random_sample(4)
-        xi[:2] = np.round(xi[:2])
-        xi[2] *= 1000.0
-        xi[3] -= 1.0
-        xxi = moop.__embed__(xi)
-        # Check that embedding is legal
-        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
-        assert(xxi.size == moop.n)
-        # Check extraction
-        assert(all(moop.__extract__(xxi) - xi < 1.0e-8))
-    # Test upper and lower bounds
-    x0 = np.zeros(4)
-    x0[:2] = np.round(x0[:2])
-    x0[2] *= 1000.0
-    x0[3] -= 1.0
-    xx0 = moop.__embed__(x0)
-    # Check that embedding is legal
-    assert(all(xx0 >= 0.0) and all(xx0 <= 1.0))
-    assert(xx0.size == moop.n)
-    # Check extraction
-    assert(all(moop.__extract__(xx0) - x0 < 1.0e-8))
-    x1 = np.ones(4)
-    x1[:2] = np.round(x1[:2])
-    x1[2] *= 1000.0
-    x1[3] -= 1.0
-    xx1 = moop.__embed__(x1)
-    # Check that embedding is legal
-    assert(all(xx1 >= 0.0) and all(xx1 <= 1.0))
-    assert(xx1.size == moop.n)
-    # Check extraction
-    assert(all(moop.__extract__(xx1) - x1 < 1.0e-8))
-
-
-def test_MOOP_embed_extract_named():
-    """ Test that the MOOP class can embed/extract named design variables.
-
-    Add several design variables and generate an embedding. Then embed and
-    extract several inputs, and check that the results match up to the
-    design tolerance. This test applies to the three hidden methods:
-     * MOOP.__embed__(x)
-     * MOOP.__extract__(x)
-     * MOOP.__generate_encoding__()
-
-    """
-
-    from parmoo import MOOP
-    from parmoo.optimizers import LocalGPS
-    import numpy as np
-
-    # Now, create another MOOP where all variables are labeled
-    moop = MOOP(LocalGPS)
-    # Add two continuous variables and check that they are embedded correctly
-    moop.addDesign({'name': "x0",
-                    'lb': 0.0,
-                    'ub': 1000.0})
-    moop.addDesign({'name': "x1",
-                    'lb': -1.0,
-                    'ub': 0.0})
-    # Test 5 random variables
-    for i in range(5):
-        nums = np.random.random_sample(2)
-        xi = np.zeros(1, dtype=[("x0", float), ("x1", float)])
-        xi["x0"] = 1000.0 * nums[0]
-        xi["x1"] = nums[1] - 1.0
-        xxi = moop.__embed__(xi)
-        # Check that embedding is legal
-        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
-        assert(xxi.size == moop.n)
-        # Check extraction
-        assert(all([moop.__extract__(xxi)[key] - xi[key] < 1.0e-8
-                    for key in ["x0", "x1"]]))
-    # Add two categorical variables and check that they are embedded correctly
-    moop.addDesign({'name': "x2",
-                    'des_type': "categorical",
-                    'levels': 2})
-    moop.addDesign({'name': "x3",
-                    'des_type': "categorical",
-                    'levels': ["biggie", "shortie", "shmedium"]})
-    # Test 5 random variables
-    for i in range(5):
-        num = np.random.random_sample(4)
-        xi = np.zeros(1, dtype=[("x0", float), ("x1", float), ("x2", float),
-                                ("x3", object)])
-        xi["x0"] = 1000.0 * num[0]
-        xi["x1"] = num[1] - 1.0
-        xi["x2"] = np.round(num[2])
-        xi["x3"] = np.random.choice(["biggie", "shortie", "shmedium"])
-        xxi = moop.__embed__(xi)
-        # Check that embedding is legal
-        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
-        assert(xxi.size == moop.n)
-        # Check extraction
-        assert(all([moop.__extract__(xxi)[key] - xi[key] < 1.0e-8
-                    for key in ["x0", "x1", "x2"]]))
-        assert(moop.__extract__(xxi)["x3"] == xi["x3"])
-
-
 def test_MOOP_addSimulation():
     """ Check that the MOOP class handles adding new simulations properly.
 
@@ -1031,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))
@@ -1145,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))
@@ -1209,10 +770,10 @@ def test_MOOP_evaluateConstraints():
                           moop2.evaluateConstraints(xx)) < 0.00000001)
 
 
-def test_MOOP_evaluateLagrangian():
-    """ Check that the MOOP class handles evaluating Lagrangian properly.
+def test_MOOP_evaluatePenalty():
+    """ Check that the MOOP class handles evaluating penalty function properly.
 
-    Initialize a MOOP object and check that the evaluateLagrangian() function
+    Initialize a MOOP object and check that the evaluatePenalty() function
     works correctly.
 
     """
@@ -1276,11 +837,11 @@ def c2(x, s, der=0):
     for i in range(3):
         moop1.addDesign({'lb': 0.0, 'ub': 1.0})
     moop1.addObjective({'obj_func': f1})
-    assert(np.all(moop1.evaluateLagrangian(np.zeros(3)) == np.zeros(1)))
-    assert(np.all(moop1.evaluateLagrangian(np.ones(3)) == 3.0 * np.ones(1)))
+    assert(np.all(moop1.evaluatePenalty(np.zeros(3)) == np.zeros(1)))
+    assert(np.all(moop1.evaluatePenalty(np.ones(3)) == 3.0 * np.ones(1)))
     moop1.addConstraint({'constraint': c1})
-    assert(np.all(moop1.evaluateLagrangian(np.zeros(3)) == np.zeros(1)))
-    assert(np.all(moop1.evaluateLagrangian(np.ones(3)) == 3.75 * np.ones(1)))
+    assert(np.all(moop1.evaluatePenalty(np.zeros(3)) == np.zeros(1)))
+    assert(np.all(moop1.evaluatePenalty(np.ones(3)) == 3.75 * np.ones(1)))
     moop1 = MOOP(LocalGPS)
     for i in range(3):
         moop1.addDesign({'lb': 0.0, 'ub': 1.0})
@@ -1290,16 +851,16 @@ def c2(x, s, der=0):
     moop1.fitSurrogates()
     moop1.addObjective({'obj_func': f1})
     moop1.addObjective({'obj_func': f1})
-    assert(np.all(moop1.evaluateLagrangian(np.zeros(3)) == np.zeros(1)))
-    assert(np.all(moop1.evaluateLagrangian(np.ones(3)) == 3.0 * np.ones(1)))
+    assert(np.all(moop1.evaluatePenalty(np.zeros(3)) == np.zeros(1)))
+    assert(np.all(moop1.evaluatePenalty(np.ones(3)) == 3.0 * np.ones(1)))
     moop1.addConstraint({'constraint': c1})
-    assert(np.all(moop1.evaluateLagrangian(np.zeros(3)) == np.zeros(1)))
-    assert(np.all(moop1.evaluateLagrangian(np.ones(3)) == 3.75 * np.ones(1)))
+    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(TypeError):
+        moop1.evaluatePenalty(10.0)
     with pytest.raises(ValueError):
-        moop1.evaluateLagrangian(10.0)
-    with pytest.raises(ValueError):
-        moop1.evaluateLagrangian(np.zeros(1))
+        moop1.evaluatePenalty(np.zeros(1))
     # Adjust the scaling and compare
     moop2 = MOOP(LocalGPS)
     moop2.addDesign({'lb': -1.0, 'ub': 1.0},
@@ -1314,8 +875,8 @@ def c2(x, s, der=0):
     moop2.addConstraint({'constraint': c1})
     x = moop1.__embed__(np.ones(3))
     xx = moop2.__embed__(np.ones(3))
-    assert(np.linalg.norm(moop1.evaluateLagrangian(x) -
-                          moop2.evaluateLagrangian(xx)) < 0.00000001)
+    assert(np.linalg.norm(moop1.evaluatePenalty(x) -
+                          moop2.evaluatePenalty(xx)) < 0.00000001)
 
 
 def test_MOOP_evaluateGradients():
@@ -1416,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))
@@ -1435,8 +996,8 @@ def c2(x, s, der=0):
     moop2.addConstraint({'constraint': c2})
     x = moop1.__embed__(np.ones(3))
     xx = moop2.__embed__(np.ones(3))
-    assert(np.linalg.norm(moop1.evaluateLagrangian(x) -
-                          moop2.evaluateLagrangian(xx)) < 0.00000001)
+    assert(np.linalg.norm(moop1.evaluatePenalty(x) -
+                          moop2.evaluatePenalty(xx)) < 0.00000001)
 
     # Initialize a MOOP with 2 SimGroups and 3 objectives with named designs
     g3 = {'n': 3,
@@ -1560,8 +1121,8 @@ def c4(x, s, der=0):
                                           ("x3", float)]))
     xx = moop4.__embed__(np.ones(1, dtype=[("x1", float), ("x2", float),
                                            ("x3", float)]))
-    assert(np.linalg.norm(moop3.evaluateLagrangian(x) -
-                          moop4.evaluateLagrangian(xx)) < 0.00000001)
+    assert(np.linalg.norm(moop3.evaluatePenalty(x) -
+                          moop4.evaluatePenalty(xx)) < 0.00000001)
 
 
 def test_MOOP_addData():
@@ -2007,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)
@@ -2704,8 +2265,14 @@ def check_moops(moop1, moop2):
     assert(all([dt2i == dt1i for dt2i, dt1i in zip(moop2.des_tols,
                                                    moop1.des_tols)]))
     assert(all([m2i == m1i for m2i, m1i in zip(moop2.m, moop1.m)]))
-    assert(all([lb2i == lb1i for lb2i, lb1i in zip(moop2.lb, moop1.lb)]))
-    assert(all([ub2i == ub1i for ub2i, ub1i in zip(moop2.ub, moop1.ub)]))
+    assert(all([lb2i == lb1i for lb2i, lb1i in zip(moop2.cont_lb,
+                                                   moop1.cont_lb)]))
+    assert(all([ub2i == ub1i for ub2i, ub1i in zip(moop2.cont_ub,
+                                                   moop1.cont_ub)]))
+    assert(all([lb2i == lb1i for lb2i, lb1i in zip(moop2.int_lb,
+                                                   moop1.int_lb)]))
+    assert(all([ub2i == ub1i for ub2i, ub1i in zip(moop2.int_ub,
+                                                   moop1.int_ub)]))
     assert(all([nl2i == nl1i for nl2i, nl1i in zip(moop2.n_lvls,
                                                    moop1.n_lvls)]))
     assert(all([do2i == do1i for do2i, do1i in zip(moop2.des_order,
@@ -2765,12 +2332,6 @@ def check_moops(moop1, moop2):
 
 if __name__ == "__main__":
     test_MOOP_init()
-    test_MOOP_addDesign_bad_cont()
-    test_MOOP_addDesign_bad_cat()
-    test_MOOP_addDesign()
-    test_MOOP_embed_extract_unnamed1()
-    test_MOOP_embed_extract_unnamed2()
-    test_MOOP_embed_extract_named()
     test_MOOP_addSimulation()
     test_pack_unpack_sim()
     test_MOOP_addObjective()
@@ -2780,7 +2341,7 @@ def check_moops(moop1, moop2):
     test_MOOP_evaluateSimulation()
     test_MOOP_evaluateSurrogates()
     test_MOOP_evaluateConstraints()
-    test_MOOP_evaluateLagrangian()
+    test_MOOP_evaluatePenalty()
     test_MOOP_evaluateGradients()
     test_MOOP_addData()
     test_MOOP_iterate()
diff --git a/parmoo/tests/unit_tests/test_moop_embeddings.py b/parmoo/tests/unit_tests/test_moop_embeddings.py
new file mode 100644
index 0000000..5055b1c
--- /dev/null
+++ b/parmoo/tests/unit_tests/test_moop_embeddings.py
@@ -0,0 +1,753 @@
+
+def test_MOOP_addDesign_bad_cont():
+    """ Check that the MOOP class handles adding bad continuous variables.
+
+    Initialize a MOOP objects, and add several bad continuous design
+    variables.
+
+    """
+
+    from parmoo import MOOP
+    from parmoo.optimizers import LocalGPS
+    import pytest
+
+    # Initialize a MOOP with no hyperparameters
+    moop = MOOP(LocalGPS)
+    # Try to add some bad design variable types
+    with pytest.raises(TypeError):
+        moop.addDesign([])
+    with pytest.raises(TypeError):
+        moop.addDesign({'des_type': 1.0})
+    with pytest.raises(ValueError):
+        moop.addDesign({'des_type': "hello world"})
+    # Add some bad continuous variables
+    with pytest.raises(AttributeError):
+        moop.addDesign({'des_type': "continuous"})
+    with pytest.raises(TypeError):
+        moop.addDesign({'des_type': "continuous",
+                        'des_tol': "hello world",
+                        'lb': 0.0,
+                        'ub': 1.0})
+    with pytest.raises(ValueError):
+        moop.addDesign({'des_type': "continuous",
+                        'des_tol': -1.0,
+                        'lb': 0.0,
+                        'ub': 1.0})
+    with pytest.raises(TypeError):
+        moop.addDesign({'des_type': "continuous",
+                        'lb': "hello",
+                        'ub': "world"})
+    with pytest.raises(ValueError):
+        moop.addDesign({'des_type': "continuous",
+                        'lb': 0.0,
+                        'ub': 0.0})
+    with pytest.raises(TypeError):
+        moop.addDesign({'name': 5,
+                        'des_type': "continuous",
+                        'lb': 0.0,
+                        'ub': 1.0})
+    # Add some bad continuous variables, using default option
+    with pytest.raises(AttributeError):
+        moop.addDesign({})
+    with pytest.raises(TypeError):
+        moop.addDesign({'des_tol': "hello world",
+                        'lb': 0.0,
+                        'ub': 1.0})
+    with pytest.raises(ValueError):
+        moop.addDesign({'des_tol': -1.0,
+                        'lb': 0.0,
+                        'ub': 1.0})
+    with pytest.raises(TypeError):
+        moop.addDesign({'lb': "hello",
+                        'ub': "world"})
+    with pytest.raises(ValueError):
+        moop.addDesign({'lb': 0.0,
+                        'ub': 0.0})
+    with pytest.raises(TypeError):
+        moop.addDesign({'name': 5,
+                        'lb': 0.0,
+                        'ub': 1.0})
+    # Try to use a repeated name to test error handling
+    with pytest.raises(ValueError):
+        moop.addDesign({'name': "x_1", 'lb': 0.0, 'ub': 1.0})
+        moop.addDesign({'name': "x_1",
+                        'des_type': "continuous",
+                        'lb': 0.0,
+                        'ub': 1.0})
+    # Add variables out of order
+    with pytest.raises(RuntimeError):
+        moop1 = MOOP(LocalGPS)
+        moop1.acquisitions.append(0)
+        moop1.addDesign({'des_type': "continuous",
+                         'lb': 0.0,
+                         'ub': 1.0})
+    with pytest.raises(RuntimeError):
+        moop2 = MOOP(LocalGPS)
+        moop2.sim_funcs.append(0)
+        moop2.addDesign({'des_type': "continuous",
+                         'lb': 0.0,
+                         'ub': 1.0})
+
+
+def test_MOOP_addDesign_bad_cat():
+    """ Check that the MOOP class handles adding bad categorical variables.
+
+    Initialize a MOOP objects, and add several bad categorical design
+    variables.
+
+    """
+
+    from parmoo import MOOP
+    from parmoo.optimizers import LocalGPS
+    import pytest
+
+    # Initialize a MOOP with no hyperparameters
+    moop = MOOP(LocalGPS)
+    # Add some bad categorical variables
+    with pytest.raises(AttributeError):
+        moop.addDesign({'des_type': "categorical"})
+    with pytest.raises(TypeError):
+        moop.addDesign({'des_type': "categorical",
+                        'levels': 1.0})
+    with pytest.raises(ValueError):
+        moop.addDesign({'des_type': "categorical",
+                        'levels': 1})
+    with pytest.raises(TypeError):
+        moop.addDesign({'name': 5,
+                        'des_type': "categorical",
+                        'levels': 2})
+    with pytest.raises(TypeError):
+        moop.addDesign({'des_type': "categorical",
+                        'levels': [3, "hi"]})
+    with pytest.raises(ValueError):
+        moop.addDesign({'des_type': "categorical",
+                        'levels': ["hi"]})
+
+
+def test_MOOP_addDesign_bad_int():
+    """ Check that the MOOP class handles adding bad integer variables.
+
+    Initialize a MOOP objects, and add several bad integer design
+    variables.
+
+    """
+
+    from parmoo import MOOP
+    from parmoo.optimizers import LocalGPS
+    import pytest
+
+    # Initialize a MOOP with no hyperparameters
+    moop = MOOP(LocalGPS)
+    # Add some bad integer variables
+    with pytest.raises(AttributeError):
+        moop.addDesign({'des_type': "integer"})
+    with pytest.raises(TypeError):
+        moop.addDesign({'des_type': "integer",
+                        'des_tol': "hello world",
+                        'lb': 0.0,
+                        'ub': 1.0})
+    with pytest.raises(TypeError):
+        moop.addDesign({'des_type': "integer",
+                        'lb': "hello",
+                        'ub': "world"})
+    with pytest.raises(ValueError):
+        moop.addDesign({'des_type': "integer",
+                        'lb': 0,
+                        'ub': 0})
+    with pytest.raises(TypeError):
+        moop.addDesign({'name': 5,
+                        'des_type': "integer",
+                        'lb': 0,
+                        'ub': 1})
+
+
+def test_MOOP_addDesign():
+    """ Check that the MOOP class handles adding design variables properly.
+
+    Initialize a MOOP objects, and add several design variables.
+
+    """
+
+    from parmoo import MOOP
+    from parmoo.optimizers import LocalGPS
+
+    # Initialize a MOOP with no hyperparameters
+    moop = MOOP(LocalGPS)
+    # Now add some continuous and integer design variables
+    assert(moop.n == 0)
+    moop.addDesign({'des_type': "continuous",
+                    'lb': 0.0,
+                    'ub': 1.0})
+    assert(moop.n == 1)
+    moop.addDesign({'des_type': "integer",
+                    'lb': 0,
+                    'ub': 4})
+    assert(moop.n == 2)
+    assert(moop.n_int == 1)
+    moop.addDesign({'name': "x_3",
+                    'des_type': "continuous",
+                    'des_tol': 0.01,
+                    'lb': 0.0,
+                    'ub': 1.0})
+    assert(moop.n == 3)
+    moop.addDesign({'lb': 0.0,
+                    'ub': 1.0})
+    assert(moop.n == 4)
+    moop.addDesign({'des_tol': 0.01,
+                    'lb': 0.0,
+                    'ub': 1.0})
+    assert(moop.n == 5)
+    moop.addDesign({'name': "x_6",
+                    'des_tol': 0.01,
+                    'lb': 0.0,
+                    'ub': 1.0})
+    assert(moop.n == 6)
+    # Now add some categorical design variables
+    assert(moop.n_cat == 0)
+    moop.addDesign({'des_type': "categorical",
+                    'levels': 2})
+    assert(moop.n_cat == 1)
+    moop.addDesign({'des_type': "categorical",
+                    'levels': 3})
+    assert(moop.n_cat == 2)
+    moop.addDesign({'name': "x_9",
+                    'des_type': "categorical",
+                    'levels': 3})
+    assert(moop.n_cat == 3)
+    moop.addDesign({'name': "x_10",
+                    'des_type': "categorical",
+                    'levels': ["boy", "girl", "doggo"]})
+    assert(moop.n_cat == 4)
+    # Now add a custom design variables
+    moop.addDesign({'des_type': "custom",
+                    'embedding_size': 1,
+                    'embedder': lambda x: x,
+                    'extracter': lambda x: x})
+    assert(moop.n_custom == 1)
+    moop.addDesign({'des_type': "raw"})
+    assert(moop.n_raw == 1)
+    # Now add more continuous design variables
+    moop.addDesign({'des_type': "continuous",
+                    'lb': 0.0,
+                    'ub': 1.0})
+    assert(moop.n_cont == 6)
+    moop.addDesign({'lb': 0.0,
+                    'ub': 1.0})
+    assert(moop.n_cont == 7)
+    # Check the design order
+    right_order = [0, 7, 1, 2, 3, 4, 8, 9, 10, 11, 12, 13, 5, 6]
+    assert(all([moop.des_order[i] == right_order[i] for i in range(14)]))
+
+
+def test_MOOP_embed_extract_unnamed1():
+    """ Test that the MOOP class can embed/extract unnamed design variables.
+
+    Add several design variables and generate an embedding. Then embed and
+    extract several inputs, and check that the results match up to the
+    design tolerance. This test applies to the three hidden methods:
+     * MOOP.__embed__(x)
+     * MOOP.__extract__(x)
+     * MOOP.__generate_encoding__()
+
+    """
+
+    from parmoo import MOOP
+    from parmoo.optimizers import LocalGPS
+    import numpy as np
+
+    # Initialize a MOOP with no hyperparameters
+    moop = MOOP(LocalGPS)
+    # Add integer and continuous vars and check that they embed correctly
+    moop.addDesign({'des_type': "integer",
+                    'lb': 0,
+                    'ub': 1000})
+    moop.addDesign({'lb': -1.0,
+                    'ub': 0.0})
+    # Test 5 random variables
+    for i in range(5):
+        xi = np.random.random_sample(2)
+        xi[0] *= 1000.0
+        xi[1] -= 1.0
+        xxi = moop.__embed__(xi)
+        # Check that embedding is legal
+        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
+        assert(xxi.size == moop.n)
+        # Check extraction
+        assert(moop.__extract__(xxi)[0] - xi[0] <= 0.5)
+        assert(moop.__extract__(xxi)[1] - xi[1] < 1.0e-8)
+    # Test upper and lower bounds
+    x0 = np.zeros(2)
+    x0[0] *= 1000.0
+    x0[1] -= 1.0
+    xx0 = moop.__embed__(x0)
+    # Check that embedding is legal
+    assert(all(xx0 >= 0.0) and all(xx0 <= 1.0))
+    assert(xx0.size == moop.n)
+    # Check extraction
+    assert(all(moop.__extract__(xx0) - x0 < 1.0e-8))
+    x1 = np.ones(2)
+    x1[0] *= 1000.0
+    x1[1] -= 1.0
+    xx1 = moop.__embed__(x1)
+    # Check that embedding is legal
+    assert(all(xx1 >= 0.0) and all(xx1 <= 1.0))
+    assert(xx1.size == moop.n)
+    # Check extraction
+    assert(all(moop.__extract__(xx1) - x1 < 1.0e-8))
+    # Add two categorical variables and check that they embed correctly
+    moop.addDesign({'des_type': "categorical",
+                    'levels': 2})
+    moop.addDesign({'des_type': "categorical",
+                    'levels': 3})
+    # Test 5 random variables
+    for i in range(5):
+        xi = np.random.random_sample(4)
+        xi[0] *= 1000
+        xi[0] = int(xi[0])
+        xi[1] -= 1.0
+        xi[2:] = np.round(xi[2:])
+        xxi = moop.__embed__(xi)
+        # Check that embedding is legal
+        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
+        assert(xxi.size == moop.n)
+        # Check extraction
+        assert(all(moop.__extract__(xxi) - xi < 1.0e-8))
+    # Test upper and lower bounds
+    x0 = np.zeros(4)
+    x0[0] *= 1000.0
+    x0[1] -= 1.0
+    x0[2:] = np.round(x0[2:])
+    xx0 = moop.__embed__(x0)
+    # Check that embedding is legal
+    assert(all(xx0 >= 0.0) and all(xx0 <= 1.0))
+    assert(xx0.size == moop.n)
+    # Check extraction
+    assert(all(moop.__extract__(xx0) - x0 < 1.0e-8))
+    x1 = np.ones(4)
+    x1[0] *= 1000.0
+    x1[1] -= 1.0
+    x1[2:] = np.round(x1[2:])
+    xx1 = moop.__embed__(x1)
+    # Check that embedding is legal
+    assert(all(xx1 >= 0.0) and all(xx1 <= 1.0))
+    assert(xx1.size == moop.n)
+    # Check extraction
+    assert(all(moop.__extract__(xx1) - x1 < 1.0e-8))
+    # Add a custom variable and raw variable and check that they embed
+    moop.addDesign({'des_type': "custom",
+                    'embedding_size': 1,
+                    'embedder': lambda x: x,
+                    'extracter': lambda x: x})
+    moop.addDesign({'des_type': "raw"})
+    # Test 5 random variables
+    for i in range(5):
+        xi = np.random.random_sample(6)
+        xi[0] *= 1000
+        xi[0] = int(xi[0])
+        xi[1] -= 1.0
+        xi[2:4] = np.round(xi[2:4])
+        xi[5] *= 5.0
+        xxi = moop.__embed__(xi)
+        # Check that embedding is legal
+        assert(all(xxi[:5] >= 0.0) and all(xxi[:5] <= 1.0))
+        assert(xxi.size == moop.n)
+        # Check extraction
+        assert(all(moop.__extract__(xxi) - xi < 1.0e-8))
+    # Test upper and lower bounds
+    x0 = np.zeros(6)
+    x0[0] *= 1000.0
+    x0[1] -= 1.0
+    x0[2:] = np.round(x0[2:])
+    x0[2:4] = np.round(xi[2:4])
+    x0[5] *= 5.0
+    xx0 = moop.__embed__(x0)
+    # Check that embedding is legal
+    assert(all(xx0 >= 0.0) and all(xx0 <= 1.0))
+    assert(xx0.size == moop.n)
+    # Check extraction
+    assert(all(moop.__extract__(xx0) - x0 < 1.0e-8))
+    x1 = np.ones(6)
+    x1[0] *= 1000.0
+    x1[1] -= 1.0
+    x1[2:] = np.round(x1[2:])
+    x1[2:4] = np.round(xi[2:4])
+    x1[5] *= 5.0
+    xx1 = moop.__embed__(x1)
+    # Check that embedding is legal
+    assert(all(xx1[:5] >= 0.0) and all(xx1[:5] <= 1.0))
+    assert(xx1.size == moop.n)
+    # Check extraction
+    assert(all(moop.__extract__(xx1) - x1 < 1.0e-8))
+
+
+def test_MOOP_embed_extract_unnamed2():
+    """ Test that the MOOP class can embed/extract unnamed design variables.
+
+    Add several design variables and generate an embedding. Then embed and
+    extract several inputs, and check that the results match up to the
+    design tolerance. This test applies to the three hidden methods:
+     * MOOP.__embed__(x)
+     * MOOP.__extract__(x)
+     * MOOP.__generate_encoding__()
+
+    """
+
+    from parmoo import MOOP
+    from parmoo.optimizers import LocalGPS
+    import numpy as np
+
+    # Same problem as in test_MOOP_embed_extract_unnamed1(), but reverse order
+    moop = MOOP(LocalGPS)
+    # Add a custom variable and raw variable and check that they embed
+    moop.addDesign({'des_type': "raw"})
+    moop.addDesign({'des_type': "custom",
+                    'embedding_size': 1,
+                    'embedder': lambda x: x,
+                    'extracter': lambda x: x})
+    # Add two categorical variables and check that they are embedded correctly
+    moop.addDesign({'des_type': "categorical",
+                    'levels': 2})
+    moop.addDesign({'des_type': "categorical",
+                    'levels': 3})
+    # Test 5 random variables
+    for i in range(5):
+        xi = np.random.random_sample(4)
+        xi[0] *= 0.5
+        xi[2:] = np.round(xi[2:])
+        xxi = moop.__embed__(xi)
+        # Check that embedding is legal
+        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
+        assert(xxi.size == moop.n)
+        # Check extraction
+        assert(all(moop.__extract__(xxi) - xi < 1.0e-8))
+    # Test upper and lower bounds
+    x0 = np.zeros(4)
+    x0[0] *= 0.5
+    x0[2:] = np.round(x0[2:])
+    xx0 = moop.__embed__(x0)
+    # Check that embedding is legal
+    assert(all(xx0 >= 0.0) and all(xx0 <= 1.0))
+    assert(xx0.size == moop.n)
+    # Check extraction
+    assert(all(moop.__extract__(xx0) - x0 < 1.0e-8))
+    x1 = np.ones(4)
+    x1[0] *= 0.5
+    x1[2:] = np.round(x1[2:])
+    xx1 = moop.__embed__(x1)
+    # Check that embedding is legal
+    assert(all(xx1 >= 0.0) and all(xx1 <= 1.0))
+    assert(xx1.size == moop.n)
+    # Check extraction
+    assert(all(moop.__extract__(xx1) - x1 < 1.0e-8))
+    # Add two continuous variables and check that they are embedded correctly
+    moop.addDesign({'lb': -1.0,
+                    'ub': 0.0})
+    moop.addDesign({'des_type': "integer",
+                    'lb': 0,
+                    'ub': 1000})
+    # Test 5 random variables
+    for i in range(5):
+        xi = np.random.random_sample(6)
+        xi[0] *= 0.5
+        xi[2:4] = np.round(xi[2:4])
+        xi[4] -= 1.0
+        xi[5] *= 1000.0
+        xxi = moop.__embed__(xi)
+        # Check that embedding is legal
+        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
+        assert(xxi.size == moop.n)
+        # Check extraction
+        assert(all(moop.__extract__(xxi) - xi < 1.0e-8))
+    # Test upper and lower bounds
+    x0 = np.zeros(6)
+    x0[0] *= 0.5
+    x0[2:4] = np.round(x0[:2])
+    x0[4] -= 1.0
+    x0[5] *= 1000.0
+    xx0 = moop.__embed__(x0)
+    # Check that embedding is legal
+    assert(all(xx0 >= 0.0) and all(xx0 <= 1.0))
+    assert(xx0.size == moop.n)
+    # Check extraction
+    assert(all(moop.__extract__(xx0) - x0 < 1.0e-8))
+    x1 = np.ones(6)
+    x1[0] *= 0.5
+    x1[2:4] = np.round(x1[2:4])
+    x1[4] -= 1.0
+    x1[5] *= 1000.0
+    xx1 = moop.__embed__(x1)
+    # Check that embedding is legal
+    assert(all(xx1 >= 0.0) and all(xx1 <= 1.0))
+    assert(xx1.size == moop.n)
+    # Check extraction
+    assert(all(moop.__extract__(xx1) - x1 < 1.0e-8))
+
+
+def test_MOOP_embed_extract_named1():
+    """ Test that the MOOP class can embed/extract named design variables.
+
+    Add several design variables and generate an embedding. Then embed and
+    extract several inputs, and check that the results match up to the
+    design tolerance. This test applies to the three hidden methods:
+     * MOOP.__embed__(x)
+     * MOOP.__extract__(x)
+     * MOOP.__generate_encoding__()
+
+    """
+
+    from parmoo import MOOP
+    from parmoo.optimizers import LocalGPS
+    import numpy as np
+
+    # Now, create another MOOP where all variables are labeled
+    moop = MOOP(LocalGPS)
+    # Add two continuous variables and check that they are embedded correctly
+    moop.addDesign({'name': "x0",
+                    'des_type': "integer",
+                    'lb': 0,
+                    'ub': 1000})
+    moop.addDesign({'name': "x1",
+                    'lb': -1.0,
+                    'ub': 0.0})
+    # Test 5 random variables
+    for i in range(5):
+        nums = np.random.random_sample(2)
+        xi = np.zeros(1, dtype=[("x0", float), ("x1", float)])
+        xi["x0"] = int(1000.0 * nums[0])
+        xi["x1"] = nums[1] - 1.0
+        xxi = moop.__embed__(xi)
+        # Check that embedding is legal
+        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
+        assert(xxi.size == moop.n)
+        # Check extraction
+        assert(all([abs(moop.__extract__(xxi)[key] - xi[key]) < 1.0e-8
+                    for key in ["x0", "x1"]]))
+    # Add two categorical variables and check that they are embedded correctly
+    moop.addDesign({'name': "x2",
+                    'des_type': "categorical",
+                    'levels': 2})
+    moop.addDesign({'name': "x3",
+                    'des_type': "categorical",
+                    'levels': ["biggie", "shortie", "shmedium"]})
+    # Test 5 random variables
+    for i in range(5):
+        num = np.random.random_sample(4)
+        xi = np.zeros(1, dtype=[("x0", float), ("x1", float), ("x2", float),
+                                ("x3", object)])
+        xi["x0"] = int(1000.0 * num[0])
+        xi["x1"] = num[1] - 1.0
+        xi["x2"] = np.round(num[2])
+        xi["x3"] = np.random.choice(["biggie", "shortie", "shmedium"])
+        xxi = moop.__embed__(xi)
+        # Check that embedding is legal
+        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
+        assert(xxi.size == moop.n)
+        # Check extraction
+        assert(all([abs(moop.__extract__(xxi)[key] - xi[key]) < 1.0e-8
+                    for key in ["x0", "x1", "x2"]]))
+        assert(moop.__extract__(xxi)["x3"] == xi["x3"])
+    # Add an integer variables and check that it is embedded correctly
+    moop.addDesign({'name': "x4",
+                    'des_type': "int",
+                    'lb': -5,
+                    'ub': 5})
+    # Test 5 random variables
+    for i in range(5):
+        num = np.random.random_sample(5)
+        xi = np.zeros(1, dtype=[("x0", float), ("x1", float), ("x2", float),
+                                ("x3", object), ("x4", int)])[0]
+        xi["x0"] = int(1000.0 * num[0])
+        xi["x1"] = num[1] - 1.0
+        xi["x2"] = np.round(num[2])
+        xi["x3"] = np.random.choice(["biggie", "shortie", "shmedium"])
+        xi["x4"] = np.random.randint(-5, 5)
+        xxi = moop.__embed__(xi)
+        # Check that embedding is legal
+        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
+        assert(xxi.size == moop.n)
+        # Check extraction
+        assert(all([abs(moop.__extract__(xxi)[key] - xi[key]) < 1.0e-8
+                    for key in ["x0", "x1", "x2", "x4"]]))
+        assert(moop.__extract__(xxi)["x3"] == xi["x3"])
+    # Add a custom variable and check that it is embedded correctly
+    moop.addDesign({'name': "x5",
+                    'des_type': "custom",
+                    'embedding_size': 1,
+                    'embedder': lambda x: float(x),
+                    'extracter': lambda x: str(x)})
+    # Test 5 random variables
+    for i in range(5):
+        num = np.random.random_sample(6)
+        xi = np.zeros(1, dtype=[("x0", float), ("x1", float), ("x2", float),
+                                ("x3", object), ("x4", int), ("x5", "U5")])[0]
+        xi["x0"] = int(1000.0 * num[0])
+        xi["x1"] = num[1] - 1.0
+        xi["x2"] = np.round(num[2])
+        xi["x3"] = np.random.choice(["biggie", "shortie", "shmedium"])
+        xi["x4"] = np.random.randint(-5, 5)
+        xi["x5"] = str(num[5])
+        xxi = moop.__embed__(xi)
+        # Check that embedding is legal
+        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
+        assert(xxi.size == moop.n)
+        # Check extraction
+        assert(all([abs(moop.__extract__(xxi)[key] - xi[key]) < 1.0e-8
+                    for key in ["x0", "x1", "x2", "x4"]]))
+        assert(moop.__extract__(xxi)["x3"] == xi["x3"])
+        assert(abs(float(moop.__extract__(xxi)["x5"]) - float(xi["x5"]))
+               < 1.0e-8)
+    # Add a raw variable
+    moop.addDesign({'name': "x6",
+                    'des_type': "raw"})
+    # Test 5 random variables
+    for i in range(5):
+        num = np.random.random_sample(7)
+        xi = np.zeros(1, dtype=[("x0", float), ("x1", float), ("x2", float),
+                                ("x3", object), ("x4", int), ("x5", "U5"),
+                                ("x6", float)])[0]
+        xi["x0"] = int(1000.0 * num[0])
+        xi["x1"] = num[1] - 1.0
+        xi["x2"] = np.round(num[2])
+        xi["x3"] = np.random.choice(["biggie", "shortie", "shmedium"])
+        xi["x4"] = np.random.randint(-5, 5)
+        xi["x5"] = str(num[5])
+        xi["x6"] = num[6]
+        xxi = moop.__embed__(xi)
+        # Check that embedding is legal
+        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
+        assert(xxi.size == moop.n)
+        # Check extraction
+        assert(all([abs(moop.__extract__(xxi)[key] - xi[key]) < 1.0e-8
+                    for key in ["x0", "x1", "x2", "x4", "x6"]]))
+        assert(moop.__extract__(xxi)["x3"] == xi["x3"])
+        assert(abs(float(moop.__extract__(xxi)["x5"]) - float(xi["x5"]))
+               < 1.0e-8)
+    # Add another custom variable and check that it is embedded correctly
+    moop.addDesign({'name': "x7",
+                    'des_type': "custom",
+                    'embedding_size': 3,
+                    'embedder': lambda x: [float(x[0]), float(x[1]),
+                                           float(x[2])],
+                    'extracter': lambda x: (str(int(x[0])) + str(int(x[1])) +
+                                            str(int(x[2])))})
+    # Test 5 random variables
+    for i in range(5):
+        num = np.random.random_sample(8)
+        xi = np.zeros(1, dtype=[("x0", float), ("x1", float), ("x2", float),
+                                ("x3", object), ("x4", int), ("x5", "U5"),
+                                ("x6", float), ("x7", "U5")])[0]
+        xi["x0"] = int(1000.0 * num[0])
+        xi["x1"] = num[1] - 1.0
+        xi["x2"] = np.round(num[2])
+        xi["x3"] = np.random.choice(["biggie", "shortie", "shmedium"])
+        xi["x4"] = np.random.randint(-5, 5)
+        xi["x5"] = str(num[5])
+        xi["x6"] = num[6]
+        xi["x7"] = "010"
+        xxi = moop.__embed__(xi)
+        # Check that embedding is legal
+        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
+        assert(xxi.size == moop.n)
+        # Check extraction
+        assert(all([abs(moop.__extract__(xxi)[key] - xi[key]) < 1.0e-8
+                    for key in ["x0", "x1", "x2", "x4", "x6"]]))
+        assert(moop.__extract__(xxi)["x3"] == xi["x3"])
+        assert(abs(float(moop.__extract__(xxi)["x5"]) - float(xi["x5"]))
+               < 1.0e-8)
+        assert(moop.__extract__(xxi)["x7"] == xi["x7"])
+
+
+def test_MOOP_embed_extract_named2():
+    """ Test that the MOOP class can embed/extract named design variables.
+
+    Add several design variables and generate an embedding. Then embed and
+    extract several inputs, and check that the results match up to the
+    design tolerance. This test applies to the three hidden methods:
+     * MOOP.__embed__(x)
+     * MOOP.__extract__(x)
+     * MOOP.__generate_encoding__()
+
+    Define the same problem as above, but add variables in reverse order.
+
+    """
+
+    from parmoo import MOOP
+    from parmoo.optimizers import LocalGPS
+    import numpy as np
+
+    # Now, create another MOOP where all variables are labeled
+    moop = MOOP(LocalGPS)
+    # Add a custom variable
+    moop.addDesign({'name': "x7",
+                    'des_type': "custom",
+                    'embedding_size': 3,
+                    'embedder': lambda x: [float(x[0]), float(x[1]),
+                                           float(x[2])],
+                    'extracter': lambda x: (str(int(x[0])) + str(int(x[1])) +
+                                            str(int(x[2])))})
+    # Add a raw variable
+    moop.addDesign({'name': "x6",
+                    'des_type': "raw"})
+    # Add a custom variable
+    moop.addDesign({'name': "x5",
+                    'des_type': "custom",
+                    'dtype': "U40",
+                    'embedding_size': 1,
+                    'embedder': lambda x: float(x),
+                    'extracter': lambda x: str(x)})
+    # Add an integer variable
+    moop.addDesign({'name': "x4",
+                    'des_type': "int",
+                    'lb': -5,
+                    'ub': 5})
+    # Add two categorical variables
+    moop.addDesign({'name': "x3",
+                    'des_type': "categorical",
+                    'levels': ["biggie", "shortie", "shmedium"]})
+    moop.addDesign({'name': "x2",
+                    'des_type': "categorical",
+                    'levels': 2})
+    # Add two continuous variables and check that they are embedded correctly
+    moop.addDesign({'name': "x1",
+                    'lb': -1.0,
+                    'ub': 0.0})
+    moop.addDesign({'name': "x0",
+                    'des_type': "integer",
+                    'lb': 0,
+                    'ub': 1000})
+    # Test 5 random variables
+    for i in range(5):
+        num = np.random.random_sample(8)
+        xi = np.zeros(1, dtype=[("x0", float), ("x1", float), ("x2", float),
+                                ("x3", object), ("x4", int), ("x5", "U5"),
+                                ("x6", float), ("x7", "U5")])[0]
+        xi["x0"] = int(1000.0 * num[0])
+        xi["x1"] = num[1] - 1.0
+        xi["x2"] = np.round(num[2])
+        xi["x3"] = np.random.choice(["biggie", "shortie", "shmedium"])
+        xi["x4"] = np.random.randint(-5, 5)
+        xi["x5"] = str(num[5])
+        xi["x6"] = num[6]
+        xi["x7"] = "010"
+        xxi = moop.__embed__(xi)
+        # Check that embedding is legal
+        assert(all(xxi >= 0.0) and all(xxi <= 1.0))
+        assert(xxi.size == moop.n)
+        # Check extraction
+        assert(all([abs(moop.__extract__(xxi)[key] - xi[key]) < 1.0e-8
+                    for key in ["x0", "x1", "x2", "x4", "x6"]]))
+        assert(moop.__extract__(xxi)["x3"] == xi["x3"])
+        assert(abs(float(moop.__extract__(xxi)["x5"]) - float(xi["x5"]))
+               < 1.0e-8)
+        assert(moop.__extract__(xxi)["x7"] == xi["x7"])
+
+
+if __name__ == "__main__":
+    test_MOOP_addDesign_bad_cont()
+    test_MOOP_addDesign_bad_cat()
+    test_MOOP_addDesign_bad_int()
+    test_MOOP_addDesign()
+    test_MOOP_embed_extract_unnamed1()
+    test_MOOP_embed_extract_unnamed2()
+    test_MOOP_embed_extract_named1()
+    test_MOOP_embed_extract_named2()
diff --git a/parmoo/tests/unit_tests/test_random_search.py b/parmoo/tests/unit_tests/test_random_search.py
index 615e7a2..78fc959 100644
--- a/parmoo/tests/unit_tests/test_random_search.py
+++ b/parmoo/tests/unit_tests/test_random_search.py
@@ -35,19 +35,19 @@ def L(z): return np.ones(2)
     def g(z): return np.ones((2, 3))
     # Create 2 acquisition functions targeting 2 "pure" solutions
     acqu1 = UniformWeights(o, lb, ub, {})
-    acqu1.setTarget({}, lambda x: np.zeros(1), {})
+    acqu1.setTarget({}, lambda x: np.zeros(2), {})
     acqu1.weights[:] = 0.0
     acqu1.weights[0] = 1.0
     acqu2 = UniformWeights(o, lb, ub, {})
-    acqu2.setTarget({}, lambda x: np.zeros(1), {})
+    acqu2.setTarget({}, lambda x: np.zeros(2), {})
     acqu2.weights[:] = 0.0
     acqu2.weights[1] = 1.0
     # Create a third acquisition function targeting a random tradeoff solution
     acqu3 = UniformWeights(o, lb, ub, {})
-    acqu3.setTarget({}, lambda x: np.zeros(1), {})
+    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,24 +55,24 @@ 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)
     opt.setConstraints(lambda z: np.asarray([0.1 - z[2], z[2] - 0.6]))
-    opt.setLagrangian(L, g)
+    opt.setPenalty(L, g)
     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_viz_without_dash.py b/parmoo/tests/unit_tests/test_viz_without_dash.py
new file mode 100644
index 0000000..024dbd4
--- /dev/null
+++ b/parmoo/tests/unit_tests/test_viz_without_dash.py
@@ -0,0 +1,912 @@
+def test_static_export():
+    """ Create a MOOP object and export various plot types.
+
+    Tests all export formats except eps
+    (because eps may not be supported on all machines).
+
+    """
+
+    from parmoo.viz.plot import (
+        scatter,
+        parallel_coordinates,
+        radar,
+    )
+    import os
+
+    # Pre-calculate a moop object
+    moop1 = run_quickstart()
+
+    # * html output
+    scatter(moop1, output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * svg output
+    parallel_coordinates(moop1, output='svg')
+    assert(os.path.exists("Pareto Front.svg"))
+    os.remove("Pareto Front.svg")
+
+    # * pdf output
+    radar(moop1, output='pdf')
+    assert(os.path.exists("Pareto Front.pdf"))
+    os.remove("Pareto Front.pdf")
+
+    # * jpeg output
+    scatter(moop1, output='jpeg')
+    assert(os.path.exists("Pareto Front.jpeg"))
+    os.remove("Pareto Front.jpeg")
+
+    # * png output
+    parallel_coordinates(moop1, output='png')
+    assert(os.path.exists("Pareto Front.png"))
+    os.remove("Pareto Front.png")
+
+    # * webp output
+    radar(moop1, output='webp')
+    assert(os.path.exists("Pareto Front.webp"))
+    os.remove("Pareto Front.webp")
+
+
+def test_quantity_constraints_objectives():
+    """ Create a MOOP object and plot PF with and w/o constraint data. """
+
+    from parmoo.viz.plot import (
+        scatter,
+        parallel_coordinates,
+        radar,
+    )
+    import os
+
+    # Pre-calculate two moop objects
+    moop1 = run_quickstart()
+    moop2 = run_dtlz2()
+
+    # * 2 objective scatter with constraint
+    scatter(moop1, output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * 2 objective parallel with constraint
+    parallel_coordinates(moop1, output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * 2 objective radar with constraint
+    radar(moop1, output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * 5 objective scatter without constraint
+    scatter(moop2, output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * 5 objective parallel without constraint
+    parallel_coordinates(moop2, output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * 5 objective radar without constraint
+    radar(moop2, output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+
+def test_database_options():
+    """ Create a MOOP object and plot the full database.
+
+    Generates plots with and without constraint violations.
+
+    """
+
+    from parmoo.viz.plot import (
+        scatter,
+        parallel_coordinates,
+        radar,
+    )
+    import os
+
+    # Pre-calculate two moop objects
+    moop1 = run_quickstart()
+    moop2 = run_dtlz2()
+
+    # * pf x constraint_satisfying x constraints in MOOP
+    scatter(
+        moop1,
+        db='pf',
+        points='constraint_satisfying',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    parallel_coordinates(
+        moop1,
+        db='pf',
+        points='constraint_satisfying',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    radar(
+        moop1,
+        db='pf',
+        points='constraint_satisfying',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * obj x constraint_satisfying x constraints in MOOP
+    scatter(
+        moop1,
+        db='obj',
+        points='constraint_satisfying',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    parallel_coordinates(
+        moop1,
+        db='obj',
+        points='constraint_satisfying',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    radar(
+        moop1,
+        db='obj',
+        points='constraint_satisfying',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    # * pf x constraint_violating x constraints in MOOP
+    scatter(
+        moop1,
+        db='pf',
+        points='constraint_violating',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    parallel_coordinates(
+        moop1,
+        db='pf',
+        points='constraint_violating',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    radar(
+        moop1,
+        db='pf',
+        points='constraint_violating',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * obj x constraint_violating x constraints in MOOP
+    scatter(
+        moop1,
+        db='obj',
+        points='constraint_violating',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    parallel_coordinates(
+        moop1,
+        db='obj',
+        points='constraint_violating',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    radar(
+        moop1,
+        db='obj',
+        points='constraint_violating',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    # * pf x all x constraints in MOOP
+    scatter(
+        moop1,
+        db='pf',
+        points='all',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    parallel_coordinates(
+        moop1,
+        db='pf',
+        points='all',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    radar(
+        moop1,
+        db='pf',
+        points='all',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * obj x all x constraints in MOOP
+    scatter(
+        moop1,
+        db='obj',
+        points='all',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    parallel_coordinates(
+        moop1,
+        db='obj',
+        points='all',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    radar(
+        moop1,
+        db='obj',
+        points='all',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    # * pf x none x constraints in MOOP
+    scatter(
+        moop1,
+        db='pf',
+        points='none',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    parallel_coordinates(
+        moop1,
+        db='pf',
+        points='none',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    radar(
+        moop1,
+        db='pf',
+        points='none',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * obj x none x constraints in MOOP
+    scatter(
+        moop1,
+        db='obj',
+        points='none',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    parallel_coordinates(
+        moop1,
+        db='obj',
+        points='none',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    radar(
+        moop1,
+        db='obj',
+        points='none',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    # * pf x constraint_satisfying x no constraints in MOOP
+    scatter(
+        moop2,
+        db='pf',
+        points='constraint_satisfying',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    parallel_coordinates(
+        moop2,
+        db='pf',
+        points='constraint_satisfying',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    radar(
+        moop2,
+        db='pf',
+        points='constraint_satisfying',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * obj x constraint_satisfying x no constraints in MOOP
+    scatter(
+        moop2,
+        db='obj',
+        points='constraint_satisfying',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    parallel_coordinates(
+        moop2,
+        db='obj',
+        points='constraint_satisfying',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    radar(
+        moop2,
+        db='obj',
+        points='constraint_satisfying',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    # * pf x constraint_violating x no constraints in MOOP
+    scatter(
+        moop2,
+        db='pf',
+        points='constraint_violating',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    parallel_coordinates(
+        moop2,
+        db='pf',
+        points='constraint_violating',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    radar(
+        moop2,
+        db='pf',
+        points='constraint_violating',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * obj x constraint_violating x no constraints in MOOP
+    scatter(
+        moop2,
+        db='obj',
+        points='constraint_violating',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    parallel_coordinates(
+        moop2,
+        db='obj',
+        points='constraint_violating',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    radar(
+        moop2,
+        db='obj',
+        points='constraint_violating',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    # * pf x all x no constraints in MOOP
+    scatter(
+        moop2,
+        db='pf',
+        points='all',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    parallel_coordinates(
+        moop2,
+        db='pf',
+        points='all',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    radar(
+        moop2,
+        db='pf',
+        points='all',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * obj x all x no constraints in MOOP
+    scatter(
+        moop2,
+        db='obj',
+        points='all',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    parallel_coordinates(
+        moop2,
+        db='obj',
+        points='all',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    radar(
+        moop2,
+        db='obj',
+        points='all',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    # * pf x none x no constraints in MOOP
+    scatter(
+        moop2,
+        db='pf',
+        points='none',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    parallel_coordinates(
+        moop2,
+        db='pf',
+        points='none',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    radar(
+        moop2,
+        db='pf',
+        points='none',
+        output='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # * obj x none x no constraints in MOOP
+    scatter(
+        moop2,
+        db='obj',
+        points='none',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    parallel_coordinates(
+        moop2,
+        db='obj',
+        points='none',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+    radar(
+        moop2,
+        db='obj',
+        points='none',
+        output='html')
+    assert(os.path.exists("Objective Data.html"))
+    os.remove("Objective Data.html")
+
+
+def test_inputs_to_dash():
+    """ Stress-test the dash app's error handling. """
+
+    from parmoo.viz.plot import scatter
+    import os
+    import pytest
+
+    # Pre-calculate a moop object
+    moop1 = run_quickstart()
+
+    # * db
+    # valid db values tested in test_database_options()
+    # test invalid db values
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', db='asdf')
+
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', db=1234)
+
+    # * output
+    # valid output values tested in test_static_export()
+    # test invalid output values
+    with pytest.raises(ValueError):
+        scatter(moop1, output='asdf')
+
+    with pytest.raises(ValueError):
+        scatter(moop1, output=1234)
+
+    # * points
+    # valid points values tested in test_database_options()
+    # test invalid points values
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', points='asdf')
+
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', points=1234)
+
+    # * height
+    # test valid height values
+    scatter(moop1, output='html', height=1)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', height=50)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', height=92.2678)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', height=7789)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # test invalid height values
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', height='asdf')
+
+    # * width
+    # test valid width values
+    scatter(moop1, output='html', width=1)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', width=50)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', width=92.2678)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', width=7789)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # test invalid width values
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', width='asdf')
+
+    # * font
+    # test valid font values
+    scatter(moop1, output='html', font='Verdana')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', font='Times New Roman')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # test invalid font values
+    # currently all inputs that can be cast to a string are valid
+    # if str(font) != a font in the computer, font=Times New Roman
+
+    # * fontsize
+    # test valid fontsize values
+    scatter(moop1, output='html', fontsize=1)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', fontsize=50)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', fontsize=55.71)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', fontsize=100)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # test invalid fontsize values
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', fontsize=-1)
+
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', fontsize=101)
+
+    # * background_color
+    # test valid background_color values
+    scatter(moop1, output='html', background_color='white')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', background_color='black')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(
+        moop1,
+        output='html',
+        background_color='transparent'
+    )
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', background_color='white')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', background_color='grey')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # test invalid background_color values
+    # currently all inputs that can be cast to a string are valid
+    # if str(background_color) != a supported background_color
+    # background_color=white
+
+    # * screenshot
+    # test valid screenshot values
+    scatter(moop1, output='html', screenshot='png')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', screenshot='jpeg')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', screenshot='svg')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', screenshot='webp')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # test invalid screenshot values
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', screenshot='asdf')
+
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', screenshot=1234)
+
+    # * image_export_format
+    # test valid image_export_format values (except eps)
+    scatter(moop1, output='html', image_export_format='html')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', image_export_format='svg')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', image_export_format='pdf')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', image_export_format='png')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', image_export_format='jpeg')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', image_export_format='webp')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # test invalid image_export_format values
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', image_export_format='asdf')
+
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', image_export_format=1234)
+
+    # * data_export_format
+    # test valid data_export_format values
+    scatter(moop1, output='html', data_export_format='csv')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', data_export_format='json')
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # test invalid data_export_format values
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', data_export_format='asdf')
+
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', data_export_format=1234)
+
+    # * dev_mode
+    # test valid dev_mode values
+    scatter(moop1, output='html', dev_mode=True)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', dev_mode=False)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # test invalid dev_mode values
+    # currently all inputs that can be cast to a boolean are valid
+    # in Python, everything can be cast to a boolean
+
+    # * pop_up
+    # test valid pop_up values
+    scatter(moop1, output='html', pop_up=True)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    scatter(moop1, output='html', pop_up=False)
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # test invalid pop_up values
+    # currently all inputs that can be cast to a boolean are valid
+    # in Python, everything can be cast to a boolean
+
+    # * port
+    # test valid port values
+    scatter(
+        moop1,
+        output='html',
+        port='http://127.0.0.1:8050/'
+    )
+    assert(os.path.exists("Pareto Front.html"))
+    os.remove("Pareto Front.html")
+
+    # test invalid port values
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', port='asdf')
+
+    with pytest.raises(ValueError):
+        scatter(moop1, output='html', port=1234)
+
+
+def run_quickstart():
+    """ Auxiliary function that creates a MOOP by running the quickstart. """
+
+    import numpy as np
+    from parmoo import MOOP
+    from parmoo.searches import LatinHypercube
+    from parmoo.surrogates import GaussRBF
+    from parmoo.acquisitions import UniformWeights
+    from parmoo.optimizers import LocalGPS
+
+    my_moop = MOOP(LocalGPS)
+
+    my_moop.addDesign({
+        'name': "x1",
+        'des_type': "continuous",
+        'lb': 0.0, 'ub': 1.0
+    })
+    my_moop.addDesign({
+        'name': "x2",
+        'des_type': "categorical",
+        'levels': 3
+    })
+
+    def sim_func(x):
+        if x["x2"] == 0:
+            return np.array([(x["x1"] - 0.2) ** 2, (x["x1"] - 0.8) ** 2])
+        else:
+            return np.array([99.9, 99.9])
+
+    my_moop.addSimulation({
+        'name': "MySim",
+        'm': 2,
+        'sim_func': sim_func,
+        'search': LatinHypercube,
+        'surrogate': GaussRBF,
+        'hyperparams': {'search_budget': 20}
+    })
+
+    my_moop.addObjective({
+        'name': "Cost of driving",
+        'obj_func': lambda x, s: s["MySim"][0]
+    })
+    my_moop.addObjective({
+        'name': "Time spent driving",
+        'obj_func': lambda x, s: s["MySim"][1]
+    })
+
+    my_moop.addConstraint({
+        'name': "c1",
+        'constraint': lambda x, s: 0.1 - x["x1"]
+    })
+
+    for i in range(3):
+        my_moop.addAcquisition({
+            'acquisition': UniformWeights,
+            'hyperparams': {}
+        })
+
+    my_moop.solve(5)
+
+    return my_moop
+
+
+def run_dtlz2():
+    """ Auxiliary function that creates a MOOP by running DTLZ2. """
+
+    from parmoo import MOOP
+    from parmoo.acquisitions import RandomConstraint
+    from parmoo.searches import LatinHypercube
+    from parmoo.surrogates import GaussRBF
+    from parmoo.optimizers import LBFGSB
+    from parmoo.objectives.dtlz import dtlz2_obj
+    from parmoo.simulations.dtlz import g2_sim
+
+    n = 6  # number of design variables
+    o = 5  # number of objectives
+    q = 4  # batch size (number of acquisitions)
+
+    # Create MOOP
+    moop = MOOP(LBFGSB)
+    # Add n design variables
+    for i in range(n):
+        moop.addDesign({
+            'name': f"Input {i+1}",
+            'des_type': 'continuous',
+            'lb': 0.0,
+            'ub': 1.0,
+            'des_tol': 1.0e-8
+        })
+
+    # Create the g2 simulation
+    moop.addSimulation({
+        'name': "g2",
+        'm': 1,
+        'sim_func': g2_sim(
+            moop.getDesignType(),
+            num_obj=o,
+            offset=0.5
+        ),
+        'search': LatinHypercube,
+        'surrogate': GaussRBF,
+        'hyperparams': {'search_budget': 10*n}
+    })
+    # Add o objectives
+    for i in range(o):
+        moop.addObjective({
+            'name': f"Objective {i+1}",
+            'obj_func': dtlz2_obj(
+                moop.getDesignType(),
+                moop.getSimulationType(),
+                i, num_obj=o)
+        })
+
+    # Add q acquisition functions
+    for i in range(q):
+        moop.addAcquisition({'acquisition': RandomConstraint})
+    # Solve the MOOP with 20 iterations
+    moop.solve(5)
+
+    return moop
+
+
+if __name__ == "__main__":
+    test_static_export()
+    test_quantity_constraints_objectives()
+    test_database_options()
+    test_inputs_to_dash()
diff --git a/parmoo/tests/unit_tests/test_weighted_sum.py b/parmoo/tests/unit_tests/test_weighted_sum.py
index 1710361..05ef3ee 100644
--- a/parmoo/tests/unit_tests/test_weighted_sum.py
+++ b/parmoo/tests/unit_tests/test_weighted_sum.py
@@ -15,44 +15,40 @@ 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):
-        acqu.setTarget(5, lambda x: np.zeros(1), {})
+    with pytest.raises(TypeError):
+        acqu.setTarget(5, lambda x: np.zeros(3), {})
     with pytest.raises(AttributeError):
-        acqu.setTarget({'x_vals': []}, lambda x: np.zeros(0), {})
+        acqu.setTarget({'x_vals': []}, lambda x: np.zeros(3), {})
     with pytest.raises(ValueError):
         acqu.setTarget({'x_vals': np.ones(1), 'f_vals': np.ones(2)},
-                       lambda x: np.zeros(0), {})
+                       lambda x: np.zeros(3), {})
     with pytest.raises(ValueError):
         acqu.setTarget({'x_vals': np.ones((1, 1)), 'f_vals': np.ones((1, 3))},
-                       lambda x: np.zeros(0), {})
+                       lambda x: np.zeros(3), {})
     with pytest.raises(ValueError):
         acqu.setTarget({'x_vals': np.ones((1, 4)), 'f_vals': np.ones((1, 1))},
-                       lambda x: np.zeros(0), {})
-    with pytest.raises(ValueError):
+                       lambda x: np.zeros(3), {})
+    with pytest.raises(TypeError):
         acqu.setTarget({}, 4, {})
     with pytest.raises(ValueError):
-        acqu.setTarget({}, 4, {})
-    with pytest.raises(ValueError):
-        acqu.setTarget({}, lambda x, y: np.zeros(1), {})
-    with pytest.raises(ValueError):
-        acqu.setTarget({}, lambda x: np.ones(1), {})
+        acqu.setTarget({}, lambda x, y: np.zeros(3), {})
     # Set a good target for future usage
-    x0 = acqu.setTarget({}, lambda x: np.zeros(1), {})
+    x0 = acqu.setTarget({}, lambda x: np.zeros(3), {})
     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))
     # Generate 3 random weight vector
     acqu1 = UniformWeights(3, np.zeros(4), np.ones(4), {})
-    acqu1.setTarget({}, lambda x: np.zeros(1), {})
+    acqu1.setTarget({}, lambda x: np.zeros(3), {})
     acqu2 = UniformWeights(3, np.zeros(4), np.ones(4), {})
-    acqu2.setTarget({}, lambda x: np.zeros(1), {})
+    acqu2.setTarget({}, lambda x: np.zeros(3), {})
     acqu3 = UniformWeights(3, np.zeros(4), np.ones(4), {})
     acqu3.setTarget({'x_vals': None, 'f_vals': None},
-                    lambda x: np.zeros(1), {})
+                    lambda x: np.zeros(3), {})
     # Check that the weights are all greater than 0
     assert(all(acqu1.weights[:] >= 0.0))
     assert(all(acqu2.weights[:] >= 0.0))
@@ -65,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)))
@@ -96,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)})
@@ -107,40 +103,36 @@ 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):
-        acqu.setTarget(5, lambda x: np.zeros(1), {})
+    with pytest.raises(TypeError):
+        acqu.setTarget(5, lambda x: np.zeros(3), {})
     with pytest.raises(AttributeError):
-        acqu.setTarget({'x_vals': []}, lambda x: np.zeros(0), {})
+        acqu.setTarget({'x_vals': []}, lambda x: np.zeros(3), {})
     with pytest.raises(ValueError):
         acqu.setTarget({'x_vals': np.ones(1), 'f_vals': np.ones(2)},
-                       lambda x: np.zeros(0), {})
+                       lambda x: np.zeros(3), {})
     with pytest.raises(ValueError):
         acqu.setTarget({'x_vals': np.ones((1, 1)), 'f_vals': np.ones((1, 3))},
-                       lambda x: np.zeros(0), {})
+                       lambda x: np.zeros(3), {})
     with pytest.raises(ValueError):
         acqu.setTarget({'x_vals': np.ones((1, 4)), 'f_vals': np.ones((1, 1))},
-                       lambda x: np.zeros(0), {})
-    with pytest.raises(ValueError):
-        acqu.setTarget({}, 4, {})
-    with pytest.raises(ValueError):
+                       lambda x: np.zeros(3), {})
+    with pytest.raises(TypeError):
         acqu.setTarget({}, 4, {})
     with pytest.raises(ValueError):
-        acqu.setTarget({}, lambda x, y: np.zeros(1), {})
-    with pytest.raises(ValueError):
-        acqu.setTarget({}, lambda x: np.ones(1), {})
+        acqu.setTarget({}, lambda x, y: np.zeros(3), {})
     # Set some good targets
     x0 = acqu.setTarget({'x_vals': None, 'f_vals': None},
-                        lambda x: np.zeros(1), {})
+                        lambda x: np.zeros(3), {})
     assert(np.all(x0[:] <= acqu.ub) and np.all(x0[:] >= acqu.lb))
     x0 = acqu.setTarget({'x_vals': np.zeros((1, 4)),
                          'f_vals': np.zeros((1, 3)),
                          'c_vals': np.zeros((1, 1))},
-                        lambda x: np.zeros(1), {})
+                        lambda x: np.zeros(3), {})
     assert(np.all(x0[:] <= acqu.ub) and np.all(x0[:] >= acqu.lb))
-    x0 = acqu.setTarget({}, lambda x: np.zeros(1), {})
+    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))
@@ -148,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)))