diff --git a/docs/examples/gp_model_creation/model_creation.ipynb b/docs/examples/gp_model_creation/model_creation.ipynb
new file mode 100644
index 00000000..5f6865a5
--- /dev/null
+++ b/docs/examples/gp_model_creation/model_creation.ipynb
@@ -0,0 +1,299 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Building GP Models from Scratch\n",
+ "Sometimes it is useful to build GP models outside the context of BO for data\n",
+ "visualization and senativity measurements, ie. learned hyperparameters. Here we\n",
+ "demonstrate how to build models from data outside of generators.\n",
+ "\n",
+ "For this we use the 3D rosenbrock function test function."
+ ],
+ "metadata": {
+ "collapsed": false
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "outputs": [
+ {
+ "data": {
+ "text/plain": " x0 x1 x2 y xopt_runtime xopt_error\n0 0.847245 -1.891988 -1.984525 3785.470594 0.000010 False\n1 0.985060 1.587642 0.141463 604.483929 0.000005 False\n2 -0.444518 0.379139 -0.225330 19.389569 0.000004 False\n3 -1.009254 1.019137 1.148092 5.235473 0.000004 False\n4 -1.981280 -1.368821 0.970260 2899.064985 0.000003 False\n5 0.786900 1.628456 -1.409226 1751.545746 0.000003 False\n6 -0.117503 -0.382464 -0.574680 70.841234 0.000003 False\n7 -1.811372 -1.028411 -1.027662 2304.022669 0.000003 False\n8 -1.078829 0.078072 -1.793674 446.984705 0.000003 False\n9 1.273585 -1.705415 1.902873 1215.692446 0.000003 False\n10 -0.431648 -1.068162 -0.023538 299.307154 0.000003 False\n11 1.166817 1.499961 1.132756 126.993414 0.000003 False\n12 1.927585 0.311807 0.537347 1179.274320 0.000003 False\n13 0.589707 0.053449 1.585747 260.280140 0.000003 False\n14 -1.518910 1.018033 0.285361 228.916244 0.000003 False",
+ "text/html": "
\n\n
\n \n \n | \n x0 | \n x1 | \n x2 | \n y | \n xopt_runtime | \n xopt_error | \n
\n \n \n \n 0 | \n 0.847245 | \n -1.891988 | \n -1.984525 | \n 3785.470594 | \n 0.000010 | \n False | \n
\n \n 1 | \n 0.985060 | \n 1.587642 | \n 0.141463 | \n 604.483929 | \n 0.000005 | \n False | \n
\n \n 2 | \n -0.444518 | \n 0.379139 | \n -0.225330 | \n 19.389569 | \n 0.000004 | \n False | \n
\n \n 3 | \n -1.009254 | \n 1.019137 | \n 1.148092 | \n 5.235473 | \n 0.000004 | \n False | \n
\n \n 4 | \n -1.981280 | \n -1.368821 | \n 0.970260 | \n 2899.064985 | \n 0.000003 | \n False | \n
\n \n 5 | \n 0.786900 | \n 1.628456 | \n -1.409226 | \n 1751.545746 | \n 0.000003 | \n False | \n
\n \n 6 | \n -0.117503 | \n -0.382464 | \n -0.574680 | \n 70.841234 | \n 0.000003 | \n False | \n
\n \n 7 | \n -1.811372 | \n -1.028411 | \n -1.027662 | \n 2304.022669 | \n 0.000003 | \n False | \n
\n \n 8 | \n -1.078829 | \n 0.078072 | \n -1.793674 | \n 446.984705 | \n 0.000003 | \n False | \n
\n \n 9 | \n 1.273585 | \n -1.705415 | \n 1.902873 | \n 1215.692446 | \n 0.000003 | \n False | \n
\n \n 10 | \n -0.431648 | \n -1.068162 | \n -0.023538 | \n 299.307154 | \n 0.000003 | \n False | \n
\n \n 11 | \n 1.166817 | \n 1.499961 | \n 1.132756 | \n 126.993414 | \n 0.000003 | \n False | \n
\n \n 12 | \n 1.927585 | \n 0.311807 | \n 0.537347 | \n 1179.274320 | \n 0.000003 | \n False | \n
\n \n 13 | \n 0.589707 | \n 0.053449 | \n 1.585747 | \n 260.280140 | \n 0.000003 | \n False | \n
\n \n 14 | \n -1.518910 | \n 1.018033 | \n 0.285361 | \n 228.916244 | \n 0.000003 | \n False | \n
\n \n
\n
"
+ },
+ "execution_count": 1,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# set values if testing\n",
+ "import os\n",
+ "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")\n",
+ "NUM_MC_SAMPLES = 1 if SMOKE_TEST else 128\n",
+ "NUM_RESTARTS = 1 if SMOKE_TEST else 20\n",
+ "\n",
+ "# Ignore all warnings\n",
+ "import warnings\n",
+ "warnings.filterwarnings(\"ignore\")\n",
+ "\n",
+ "from xopt import Xopt, Evaluator\n",
+ "from xopt.generators import RandomGenerator\n",
+ "from xopt.resources.test_functions.rosenbrock import evaluate_rosenbrock,make_rosenbrock_vocs\n",
+ "\n",
+ "# make rosenbrock function vocs in 3D\n",
+ "vocs = make_rosenbrock_vocs(3)\n",
+ "\n",
+ "# collect some data using random sampling\n",
+ "evaluator = Evaluator(function=evaluate_rosenbrock)\n",
+ "generator = RandomGenerator(vocs=vocs)\n",
+ "X = Xopt(generator=generator, evaluator=evaluator, vocs=vocs)\n",
+ "X.random_evaluate(15)"
+ ],
+ "metadata": {
+ "collapsed": false,
+ "ExecuteTime": {
+ "end_time": "2024-04-24T14:45:11.242373800Z",
+ "start_time": "2024-04-24T14:45:05.850560800Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Create GP model based on the data"
+ ],
+ "metadata": {
+ "collapsed": false
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "outputs": [],
+ "source": [
+ "data = X.data"
+ ],
+ "metadata": {
+ "collapsed": false,
+ "ExecuteTime": {
+ "end_time": "2024-04-24T14:45:11.261375100Z",
+ "start_time": "2024-04-24T14:45:11.244374300Z"
+ }
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Help on method build_model in module xopt.generators.bayesian.models.standard:\n",
+ "\n",
+ "build_model(input_names: List[str], outcome_names: List[str], data: pandas.core.frame.DataFrame, input_bounds: Dict[str, List] = None, dtype: torch.dtype = torch.float64, device: Union[torch.device, str] = 'cpu') -> botorch.models.model_list_gp_regression.ModelListGP method of xopt.generators.bayesian.models.standard.StandardModelConstructor instance\n",
+ " Construct independent models for each objective and constraint.\n",
+ " \n",
+ " Parameters\n",
+ " ----------\n",
+ " input_names : List[str]\n",
+ " Names of input variables.\n",
+ " outcome_names : List[str]\n",
+ " Names of outcome variables.\n",
+ " data : pd.DataFrame\n",
+ " Data used for training the model.\n",
+ " input_bounds : Dict[str, List], optional\n",
+ " Bounds for input variables.\n",
+ " dtype : torch.dtype, optional\n",
+ " Data type for the model (default is torch.double).\n",
+ " device : Union[torch.device, str], optional\n",
+ " Device on which to perform computations (default is \"cpu\").\n",
+ " \n",
+ " Returns\n",
+ " -------\n",
+ " ModelListGP\n",
+ " A list of trained botorch models.\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "from xopt.generators.bayesian.models.standard import StandardModelConstructor\n",
+ "model_constructor = StandardModelConstructor()\n",
+ "\n",
+ "# here we build a model from vocs\n",
+ "model = model_constructor.build_model_from_vocs(\n",
+ " vocs=vocs, data=data\n",
+ ")\n",
+ "\n",
+ "# here we build a model from info (more flexible)\n",
+ "model = model_constructor.build_model(\n",
+ " input_names=[\"x0\",\"x1\",\"x2\"],\n",
+ " outcome_names=[\"y\"],\n",
+ " data=data\n",
+ ")\n",
+ "help(model_constructor.build_model)\n"
+ ],
+ "metadata": {
+ "collapsed": false,
+ "ExecuteTime": {
+ "end_time": "2024-04-24T14:45:13.955823900Z",
+ "start_time": "2024-04-24T14:45:11.260373200Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Examine GP model hyperparameters\n",
+ "Here we look at the GP hyperparameters for the objective function (the first model).\n",
+ "Note: the hyperparameters here are in raw_units (due to contraints on parameter\n",
+ "values, ie. lengthscales > 0)"
+ ],
+ "metadata": {
+ "collapsed": false
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "likelihood.noise_covar.raw_noise Parameter containing:\n",
+ "tensor([-21.6353], dtype=torch.float64, requires_grad=True)\n",
+ "mean_module.raw_constant Parameter containing:\n",
+ "tensor(0.6334, dtype=torch.float64, requires_grad=True)\n",
+ "covar_module.raw_outputscale Parameter containing:\n",
+ "tensor(0.7650, dtype=torch.float64, requires_grad=True)\n",
+ "covar_module.base_kernel.raw_lengthscale Parameter containing:\n",
+ "tensor([[-0.9368, -0.5085, -0.0153]], dtype=torch.float64, requires_grad=True)\n"
+ ]
+ }
+ ],
+ "source": [
+ "objective_model = model.models[vocs.output_names.index(\"y\")]\n",
+ "\n",
+ "# print raw hyperparameter values\n",
+ "for name,val in objective_model.named_parameters():\n",
+ " print(name,val)"
+ ],
+ "metadata": {
+ "collapsed": false,
+ "ExecuteTime": {
+ "end_time": "2024-04-24T14:45:13.998821Z",
+ "start_time": "2024-04-24T14:45:13.958825500Z"
+ }
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "outputscale: tensor(1.1471, dtype=torch.float64)\n",
+ "prior mean: tensor(0.6334, dtype=torch.float64)\n",
+ "noise: tensor([0.0001], dtype=torch.float64)\n",
+ "lengthscales tensor([[0.3306, 0.4709, 0.6855]], dtype=torch.float64)\n"
+ ]
+ }
+ ],
+ "source": [
+ "# print real values - note that these are in normalized coordinates\n",
+ "print(\"outputscale:\", objective_model.covar_module.outputscale.data)\n",
+ "print(\"prior mean:\", objective_model.mean_module.constant.data)\n",
+ "print(\"noise:\", objective_model.likelihood.noise_covar.noise.data)\n",
+ "print(\"lengthscales\", objective_model.covar_module.base_kernel.lengthscale.data)"
+ ],
+ "metadata": {
+ "collapsed": false,
+ "ExecuteTime": {
+ "end_time": "2024-04-24T14:45:14.007821800Z",
+ "start_time": "2024-04-24T14:45:13.974821300Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Visualize model predictions"
+ ],
+ "metadata": {
+ "collapsed": false
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "outputs": [
+ {
+ "data": {
+ "text/plain": "