From e45cb5478d40344d1f5a16a86395f555fbc20d9a Mon Sep 17 00:00:00 2001 From: Ryan Roussel Date: Tue, 17 Mar 2026 15:06:24 -0500 Subject: [PATCH 01/16] first commit --- .../gp_model_creation/model_creation.ipynb | 4 +- docs/examples/gp_model_creation/saas.ipynb | 526 ++++++++++++++++++ xopt/generators/bayesian/base_model.py | 114 +++- xopt/generators/bayesian/models/__init__.py | 8 +- xopt/generators/bayesian/models/saas.py | 465 ++++++++++++++++ xopt/generators/bayesian/models/standard.py | 28 + .../bayesian/test_model_constructor.py | 11 +- 7 files changed, 1150 insertions(+), 6 deletions(-) create mode 100644 docs/examples/gp_model_creation/saas.ipynb create mode 100644 xopt/generators/bayesian/models/saas.py diff --git a/docs/examples/gp_model_creation/model_creation.ipynb b/docs/examples/gp_model_creation/model_creation.ipynb index 9ab3ee3e4..a4180f151 100644 --- a/docs/examples/gp_model_creation/model_creation.ipynb +++ b/docs/examples/gp_model_creation/model_creation.ipynb @@ -221,7 +221,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "xopt-dev", "language": "python", "name": "python3" }, @@ -235,7 +235,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.11" + "version": "3.14.2" } }, "nbformat": 4, diff --git a/docs/examples/gp_model_creation/saas.ipynb b/docs/examples/gp_model_creation/saas.ipynb new file mode 100644 index 000000000..fd50beea3 --- /dev/null +++ b/docs/examples/gp_model_creation/saas.ipynb @@ -0,0 +1,526 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "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." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\Ryan Roussel\\Documents\\GitHub\\Xopt\\xopt\\pydantic.py:39: UserWarning: Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater.\n", + " from pydantic.v1.json import custom_pydantic_encoder\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
x0x1x2yxopt_runtimexopt_error
01.441835-1.899973-1.3221605.6887873.499968e-06False
10.038882-0.062271-0.0032260.0053901.900014e-06False
2-0.032775-0.712506-1.8203040.5087391.200009e-06False
3-1.714610-0.6104391.0374053.3125239.000069e-07False
4-1.606494-0.620783-1.8383862.9661941.000008e-06False
5-0.553693-1.6406811.7094422.9984109.000069e-07False
6-0.6903900.726443-1.9609291.0043599.000069e-07False
70.067712-0.836368-0.7161850.7040977.000053e-07False
8-1.9846100.3429961.9600664.0563258.000061e-07False
91.861432-0.0700511.4609653.4698377.000053e-07False
101.847126-0.1115691.8200803.4243238.000061e-07False
11-0.2557350.162920-0.5875930.0919431.000008e-06False
12-0.0047481.217890-0.0496321.4832809.000069e-07False
13-1.3940590.2848501.3991672.0245409.000069e-07False
140.8296231.3067971.7292352.3959948.000061e-07False
\n", + "
" + ], + "text/plain": [ + " x0 x1 x2 y xopt_runtime xopt_error\n", + "0 1.441835 -1.899973 -1.322160 5.688787 3.499968e-06 False\n", + "1 0.038882 -0.062271 -0.003226 0.005390 1.900014e-06 False\n", + "2 -0.032775 -0.712506 -1.820304 0.508739 1.200009e-06 False\n", + "3 -1.714610 -0.610439 1.037405 3.312523 9.000069e-07 False\n", + "4 -1.606494 -0.620783 -1.838386 2.966194 1.000008e-06 False\n", + "5 -0.553693 -1.640681 1.709442 2.998410 9.000069e-07 False\n", + "6 -0.690390 0.726443 -1.960929 1.004359 9.000069e-07 False\n", + "7 0.067712 -0.836368 -0.716185 0.704097 7.000053e-07 False\n", + "8 -1.984610 0.342996 1.960066 4.056325 8.000061e-07 False\n", + "9 1.861432 -0.070051 1.460965 3.469837 7.000053e-07 False\n", + "10 1.847126 -0.111569 1.820080 3.424323 8.000061e-07 False\n", + "11 -0.255735 0.162920 -0.587593 0.091943 1.000008e-06 False\n", + "12 -0.004748 1.217890 -0.049632 1.483280 9.000069e-07 False\n", + "13 -1.394059 0.284850 1.399167 2.024540 9.000069e-07 False\n", + "14 0.829623 1.306797 1.729235 2.395994 8.000061e-07 False" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# set values if testing\n", + "import os\n", + "\n", + "from xopt import Xopt, Evaluator\n", + "from xopt.generators import RandomGenerator\n", + "from xopt.resources.test_functions.rosenbrock import (\n", + " make_rosenbrock_vocs,\n", + ")\n", + "\n", + "from xopt.generators.bayesian.visualize import visualize_model\n", + "from xopt.generators.bayesian.models import StandardModelConstructor\n", + "\n", + "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")\n", + "\n", + "# make rosenbrock function vocs in 3D\n", + "vocs = make_rosenbrock_vocs(3)\n", + "\n", + "def evaluate_func(X):\n", + " return {\"y\": X[\"x0\"] ** 2 + X[\"x1\"] ** 2}\n", + "\n", + "# collect some data using random sampling\n", + "evaluator = Evaluator(function=evaluate_func)\n", + "generator = RandomGenerator(vocs=vocs)\n", + "X = Xopt(generator=generator, evaluator=evaluator)\n", + "X.random_evaluate(15)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Standard Model Constructor" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "A class for constructing independent models for each objective and constraint.\n", + "\n", + "Attributes\n", + "----------\n", + "name : str\n", + " The name of the model (frozen).\n", + "\n", + "use_low_noise_prior : bool\n", + " Specify if the model should assume a low noise environment.\n", + "\n", + "covar_modules : Dict[str, Kernel]\n", + " Covariance modules for GP models.\n", + "\n", + "mean_modules : Dict[str, Module]\n", + " Prior mean modules for GP models.\n", + "\n", + "trainable_mean_keys : List[str]\n", + " List of prior mean modules that can be trained.\n", + "\n", + "transform_inputs : Union[Dict[str, bool], bool]\n", + " Specify if inputs should be transformed inside the GP model. Can optionally\n", + " specify a dict of specifications.\n", + "\n", + "custom_noise_prior : Optional[Prior]\n", + " Specify a custom noise prior for the GP likelihood. Overwrites value specified\n", + " by use_low_noise_prior.\n", + "\n", + "use_cached_hyperparameters : Optional[bool]\n", + " Flag to specify if cached hyperparameters should be used in model creation.\n", + " Training will still occur unless train_model is False.\n", + "\n", + "train_method : Literal[\"lbfgs\", \"adam\"]\n", + " Numerical optimization algorithm to use.\n", + "\n", + "train_model : bool\n", + " Flag to specify if the model should be trained (fitted to data).\n", + "\n", + "train_config : NumericalOptimizerConfig\n", + " Configuration of the numerical optimizer.\n", + "\n", + "\n" + ] + } + ], + "source": [ + "print(StandardModelConstructor.__doc__)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "## Create GP model based on the data" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "data = X.data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model_constructor = StandardModelConstructor(saas_outputs=[\"y\"])\n", + "\n", + "# here we build a model from info (more flexible)\n", + "model = model_constructor.build_model(\n", + " input_names=vocs.variable_names, \n", + " outcome_names=[\"y\"], \n", + " data=data, \n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "likelihood.noise_covar.raw_noise Parameter containing:\n", + "tensor([-9.2103], requires_grad=True)\n", + "mean_module.raw_constant Parameter containing:\n", + "tensor(0.0375, requires_grad=True)\n", + "covar_module.raw_outputscale Parameter containing:\n", + "tensor(7.7638, requires_grad=True)\n", + "covar_module.base_kernel.raw_lengthscale Parameter containing:\n", + "tensor([[1.7046, 1.8655, 9.2103]], requires_grad=True)\n", + "covar_module.base_kernel.raw_tau Parameter containing:\n", + "tensor(-6.9078, 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)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "## Visualize model predictions" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "findfont: Font family ['DejaVu Sans Display'] not found. Falling back to DejaVu Sans.\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxUAAAFvCAYAAAA8O+aEAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAArhBJREFUeJztnQecU1X2x0/K9IGhD70jRZAOUhQQFBVd2/oHFwUV0WXBXlZcFduKXRQVRAUbLIhdVJQqCkhHaaLS69Db1JT3/5yLCSknmZvJTCbl9/XzHHLffSUvybnvvHt+55gMwzAIAAAAAAAAAEqIuaQbAgAAAAAAAACcCgAAAAAAAEDYYKYCAAAAAAAAEBZwKgAAAAAAAABhAacCAAAAAAAAEBZwKgAAAAAAAABhAacCAAAAAAAAEBZwKgAAAAAAAABhAacCAAAAAAAAEBZwKgAACccff/xBF110EWVlZZHJZKLPP/+8vE8prrjxxhupYcOG5X0aAAAAIgicCgBA1PLuu++qm37XYrVaqU6dOuqmdc+ePSXe79ChQ2ndunX03//+lz744APq1KlTqZ43KDnffPMNPfbYYxG5hLt27aLHH3+cunTpQpUrV6Zq1apR7969ae7cuWHtd9++ffTggw9Snz59qEKFCuq7u3DhQu3t2SHz/N57Ls2aNQvr3AAAoKywltmeAQCglHjiiSeoUaNGVFBQQD///LNyNn766Sdav349paamhrSv/Px8Wrp0Kf3nP/+hUaNG4TMqA9566y1yOp0ldipef/31iDgWX3zxBT377LN05ZVXKkfTbrfT+++/TxdeeCFNnjyZbrrpphLtd/PmzWq/7AC0adNGfd9CYdy4cXTq1Cmvth07dtDDDz+sZtgAACAagVMBAIh6LrnkEvdswi233KKeKPNN25dffkn/93//F9K+Dh48qP5WqlSp1M6PnZ3k5GQymzH5yyQlJVEswDMJO3fuVN8nF//85z+pXbt29Oijj5bYqejYsSMdPnyYqlSpQh9//DFde+21IW3PTo4vTz31lPo7ePDgEp0TAACUNRgBAQAxx3nnnaf+btmyxav9t99+o7///e/qZo5nMNgRYcfDBT/9btCggfr3/fffr8JJPGP/OaTq5ptvpuzsbEpJSaGzzz5bPbH2hMNYeLvp06erJ8ccjpWenk4nTpxQ65ctW0YXX3yx0mtwe69evWjx4sVe++Dz4H38+eefKpSLHRzuzzexeXl5fu/3ww8/VCE6vD8O0zn//PPp+++/9+rz7bffquuSkZGhQm4GDBhAGzZs0A4xW7RoEd12221UtWpVqlixIg0ZMoSOHj3q1/+NN95Q14WvT+3atWnkyJF07NixoJqK7du3q2O88MILNGnSJGrSpInavnPnzrRixQqv7XiWgvEM+fEMK+LP2GazBX1PY8aMUQ7evHnzvNpvvfVW5fz98ssv6jW/D0+HguHzuvTSS2n37t108uRJdzsfk4/N51AcfP35O1iaTJs2Tc3Wde/evVT3CwAApQVmKgAAMQffpDJ8g+2Cb6B79OihbvI5np1vrj/66CP11PeTTz6hq666iq6++mp1A3/33XfTddddp24eMzMz1fY5OTl07rnnqptYDouqXr26ulEfNmyYchjuuusur3N48skn1Q3qfffdR4WFherf8+fPV7Mq/KTadWM7ZcoUuuCCC+jHH39UjoEnPMvCN4pjx46l1atX09tvv001atRQszAuOOafnRC+meQwMD4OOy58LFcoDOtCOHynf//+alt2TCZMmEA9e/akNWvWaImm+T3zteFjcfgOb88hNy4niuF1fD79+vWjESNGuPuxY8COU3EzFHxjzDfq7LzwPp977jn1mWzdulVty+179+6lOXPmqPfky+jRo+m9996jbdu2BX1P7Ox99dVX6rNj7Qzf5H/33XcqLIs/t7Zt2wY9z/379ysHjhdPh7Nly5bqOrMjFkn4M9y0aZMK2QMAgKjFAACAKGXKlCkGm6m5c+caBw8eNHbt2mV8/PHHRvXq1Y2UlBT12kXfvn2NNm3aGAUFBe42p9NpdO/e3WjWrJm7bdu2bWqfzz//vNexhg0bZtSqVcs4dOiQV/ugQYOMrKwsIy8vT71esGCB2r5x48buNtex+Dj9+/dX/3bBfRo1amRceOGF7rYxY8aofdx8881ex7rqqquMqlWrul//8ccfhtlsVu0Oh8Orr+sYJ0+eNCpVqmQMHz7ca/3+/fvVefu2B7rGHTt2NIqKitztzz33nGr/4osv1OsDBw4YycnJxkUXXeR1Lq+99prqN3nyZHfb0KFDjQYNGvhdc35vR44ccbfzvrn9q6++creNHDlStUnwfnkd76841q1bp873lltuMY4ePWrUqVPH6NSpk2Gz2YJux9c8NTXVuOGGG7zaXe+BzyEUZs6cqbbj701Juffee9U+Nm7cWOJ9AABAWYPwJwBA1MNPxnnmoF69eiq8iWchOKypbt26av2RI0fUk3t+8s9Pwg8dOqQWjmvnp/ecQjZYtijDMNRsxuWXX67+7dqeF97++PHjaibBE35inZaW5n69du1adZx//OMf6riu7XNzc6lv374qvMhXvMzx+55w+BJv6wql4lS3vA3H9/vqNVyzB/xUn8OPeObF87wtFgt17dqVFixYoHWNOTTIc6aBZyI42xYLpxnOiFRUVKRmbDzPZfjw4Spc6uuvvy72GAMHDvSaXXKFsfFMhQ48Q8Cfj87MS+vWrdWsCs/+8GfI14RnOfg9BYJneFj/wJ/rM88847WOj8nHjvQsBX/+HGrXvn17NVMCAADRCsKfAABRD8fZn3XWWermnjUOfIPOse8uWJvAN3yPPPKIWiQOHDigQqMCibf5xpzj/XkJtL0nHLbkCTsULmcjEHz+njfV9evX91rvWsdaBr5RZ80I38C3atUq4D5dx+UQKwnejw6+qUo5LKxWrVruUDMOhWKaN2/u1Y/DsRo3buxeH4xg77csYN0M35AvX76cnn766aDX0eFw0KBBg2jjxo0q7I31ItHADz/8oBxiDtkDAIBoBk4FACDqYS2CK/sTayRYK8AzAhzTzze/rhkA1jfwU2mJpk2bBty/a/vrr78+oFNwzjnneL32nKXw3Mfzzz+vsgdJuPQbLng2QYIdJF1cx2UNQs2aNf3WB3syH2lK4/2GAs+AuJwu1lYEg2dcZs2aRVOnTg3ooJUHfD7sWPJMFAAARDPRM9oAAIDmjSkLmzkd6GuvvaZE2fyknOHwHQ6VChUOrWIxLz+tLsn2DGc0cs0MlHQf0j7ZaeCn54EcFddxWeAdznH55puvqQuuk8CZjljMzriyZrEj57reDIdEsXC6tN6zZ7ancODrxtmk+PPgkC2eqeDQORaGSzMaLKjn+hDRdPPOCQA4LI8L8kXLzAkAAAQCmgoAQMzBN1k8e8E3gVwjgm+oue3NN98UU366alMEc1SuueYadQPHBfVC3Z7hjE98g89pU30Ll+nuwxeeleGn1Jz1yVeP4Xq6zzMzfOPMN81SqlXd43LYl+f2nNWJi8FxNiuGnQYOdXr11Ve9ZhbeeecdFdbFKWxLA9bLML5pakNJKcu89NJLtGTJEvW+OOMTZ89inQhrKzzhmSX+zB566CG68847A+4vlJSyocB1Mni/Eqxn4euA2hQAgFgAMxUAgJiEny6zqJaFsyx4Zt0Fh0VxBWMOZeGn6ZwmlqsZc80BV22CQLAwl0XNLG7m7Tn+ngXgLNBmkTL/Oxh888+iYL4J5/oHXHOCNRwcD8/75Rt/TnMaChyyxWlE+aaYRc38lJ21JJzClZ9c84wN75cdgBtuuIE6dOigdAE888I3qyye5jS7PKNTHDzjwIJyFrvzbATXo+Dr+be//U2t531ySlcWP3MdDm539eN6Exw6Vhqwc8bccccdymFih4/fUygpZTn9KmtreKaCxfcMf094tudf//qXSjXMfPbZZ/TAAw8oPQmLoLkeiCdcWZtrlpQkpayrWJ2rVgiHp3EVeFfKWxdcD4R1E1IIGIc+8efNDi8AAEQ9ZZ5fCgAASogr3emKFSv81nFa0yZNmqjFbrerti1bthhDhgwxatasaSQlJak0opdddplKQ1tcSlkmJydHpTStV6+e2p73w6lqJ02a5O7jSinLqUIl1qxZY1x99dUqfSqnveXUqv/3f/9nzJs3zy+lLKfJld6vb8pUTtfavn17tb/KlSsbvXr1MubMmePVh8+L09lyGllOicrX5cYbbzRWrlypdY1/+OEH49Zbb1X7z8zMNAYPHmwcPnzYrz+nkG3RooW6PtnZ2caIESNUylZPAqWUla45t/P1cMGf5e23367SBptMJq/0sjopZXn7zp07G3Xr1jWOHTvmte6VV15R28+YMcPrcwi0eKaBDTWlbLD9esKfpTQUHz9+XH2O/F0CAIBYwMT/K2/HBgAAQPnAT915VoVnP1xieAAAACBUoKkAAAAAAAAAhAWcCgAAAAAAAEBYwKkAAAAAAAAAhAU0FQAAAAAAAICwwEwFAAAAAAAAICzgVAAAAAAAAADCAk4FAAAAAAAAICzgVAAAAAAAAADCAk4FAAAAAAAAICzgVAAAAAAAAADCAk4FAAAAAAAAICzgVAAAAAAAAADCAk4FAAAAAAAAICzgVAAAAAAAAADCAk4FAAAAAAAAICzgVAAAAAAAAADCAk4FAAAAAAAAICzgVAAAAAAAAADCAk4FAAAAAAAAICzgVAAAAAAAAADCAk4FAAAAAAAAICzgVAAAAAAAAADCAk4FAAAAAAAAICzgVAAAAAAAAADCAk4FAAAAAAAAICzgVAAAAAAAAADCAk4FKFfeffddMplMtH37dnwSmixcuFBdM9eycuXKkK/dlVde6d6+devWuPYAgKggnscEfk+etvvjjz8OeR933XWXe/vMzMwyOU8ASgqcihg1uK4lNTWVzjrrLBo1ahTl5OSU+vHy8vLoscceUzeysQ6/D75mZrOZdu3a5bf+xIkTlJaWpvrw9Yx2HnroIfrggw+ocePGIW979913q21btGhRJucGAIgMGBPC46effqJLLrmE6tSpo8bT+vXr0+WXX07Tpk3z6uc57lqtVqpSpQp17NiR7rzzTtq4cWNIx7z11luV/e3SpUvI53vDDTeobc8777yQtwWgrIFTEaM88cQTyrC89tpr1L17d5owYQJ169ZNOQGlCe/v8ccfLzOngg1kfn4+NWjQgCJFSkoK/e9///Nr//TTTymWuPDCC+n6669Xg1uo9OrVS22bnZ1dJucGAIgsGBNCZ+bMmXT++eerB3LsHIwfP17ZxaNHj9Jbb70l2lwed6dMmaKud4cOHei9996jtm3b0ksvvaR9XB6r+TjswIQKOzK8bUkeJgFQ1ljL/AigTOAnK506dVL/vuWWW6hq1arKqH3xxRd03XXXRf1Vz83NpYyMDLJYLGopTScoPT09aJ9LL71UORUPPPCAVzs/mRowYAB98sknpXY+AAAQCTAmhD4m8Ox1q1at6Oeff6bk5GSvdQcOHPDrz1EBfEPvyTPPPKNmNu69914188vjCwCJCmYq4oQLLrhA/d22bZv6a7fb6cknn6QmTZqoJ/MNGzZU4TKFhYVe23E8fv/+/alatWoq9KdRo0Z08803u+M/q1evrv7NsxWuqV82xC5+++03+vvf/66elvPUMTs6X375pTg9/8MPP9C//vUvqlGjBtWtWzdo/Owbb7xBZ599tjr32rVr08iRI+nYsWNefXr37q30AKtWrVJPm3jg4PdYHP/4xz9o7dq16txd7N+/n+bPn6/WSfB1GzNmDDVt2lSdU7169ZRT4ns9+QkWfxb8HrkfD1g8i+QLfx6XXXaZmnrnKXC+dvzk6f3336eSwsfma7lmzRq/dU8//bRy3vbs2VPi/QMAYgeMCcWPCVu2bKHOnTv7ORQM23Ad+IHe9OnTVUjUf//73xJ9VgsWLFC2+7PPPvNbxw+7eN3SpUtLtG8AIglmKuIENo4uA+eaveBpWb7h5ycoy5Yto7Fjx9KmTZvchoufxFx00UXKcXjwwQepUqVK6ubeFQbE7XxDPGLECLrqqqvo6quvVu3nnHOO+rthwwbq0aOHikXl7Xnm4aOPPlIiYH7az9t4wg4F7/PRRx9VMxWBYKeFnZh+/fqpY2/evFmdx4oVK2jx4sWUlJTk7nv48GH1hG7QoEHa4TzsgLBTw8aap7CZGTNmKNEbz1T44nQ66W9/+5tyADgWtmXLlrRu3Tp6+eWX6ffff6fPP//c3ZfPk50h7s+DzFdffaXeN++DHSNP/vzzT/X5DBs2jIYOHUqTJ0+mG2+8UU1v8z5ChffFx5g6dSq1b9/eax23sRPGnxUAIP7BmFD8mMBht/PmzaPdu3e7H3SVBA5j4pBSdg5Ym1exYsWQtmfbzA+q2E77jpvcxg8HOWQKgKjHADHFlClTDP7Y5s6daxw8eNDYtWuXMX36dKNq1apGWlqasXv3bmPt2rWqzy233OK17X333afa58+fr15/9tln6vWKFSsCHo+PwX3GjBnjt65v375GmzZtjIKCAneb0+k0unfvbjRr1szvnHv27GnY7Xbx/Wzbtk29PnDggJGcnGxcdNFFhsPhcPd77bXXVL/Jkye723r16qXaJk6cqHXt+D1wf35PfC2aNm3qXte5c2fjpptuUv/mPiNHjnSv++CDDwyz2Wz8+OOPXvvj43LfxYsXu9vy8vL8jtu/f3+jcePGXm0NGjRQ2y5atMjdxu89JSXFuPfee4O+jwULFqht+a8v1113nVG7dm2va7d69WrVn6+1L3wNzz777KDHAwBELxgTSj4mvPPOO6o/jzl9+vQxHnnkEWXnPe2nC99xwZc777xT9fnll18C9uFxLpAtHj16tLL/x44d8xoTrFarOP4OHTrUyMjI0HqfAEQKhD/FKPwUn5/689MNfkrPT9l5BoKfRH/zzTeqzz333OO1Dc9YMF9//bX6yzMTzKxZs8hms4V0/CNHjqhwof/7v/+jkydP0qFDh9TCMwccTvXHH3/4hdoMHz68WP3E3LlzqaioSKXN4yxNntvy0x/XubvgEKObbrqJQoXDnHimgGc/XH8DhT6xmI9nJzhe1vU+eXGFF/DTKRccQubi+PHjqh8/wdq6dat67QmHRnlm8ODPs3nz5qpvSRkyZAjt3bvX65z4SRef1zXXXFPi/QIAohuMCaGPCRzqO3v2bDVTwDPRHDLMNrlZs2a0ZMmSkK6/K70rj4cltd0cTuuZZpZn0DmU2VfHAUC0gvCnGOX1119XojEOseHpXb4Zdd2E79ixQ/2b4/89qVmzpnIkeD3DN7t8o8mhRhzKw4aVQ5f45poNczD4Rpwf3jzyyCNqkeDwKs9wG9ZrFIfr3Pj9eMIxr6w5cK13wfuX4mGLg8OD2EngECi+JnxtXE6CL+wgcdiYS1/ii6egj8OzWHvB8a++mbjYqcjKynK/ljJ/VK5cWWUeKSmcnaRWrVrKkejbt68Ku2JR+hVXXEEVKlQo8X4BANENxoSSjQn8EIwXttesz+Mb+YkTJyrNG+vudLUVp06dUn9Lamd5PGJ9B9tuDoll+N/nnnuu31gOQLQCpyJGYXGvK/tTIFjcVdx6firCmS849v+7775TT25efPFF1RassA7frDL33XefMsgSvobQ8yl+aRHOPtl5Yg0EDwIDBw70mhnxfa9t2rQJmDKQZ4tcMcx8I8+DA/fldh7ceOaInTbXNXMRaNbm9Ex7yeB98vvidIgsdmcnh2cu8KQLgPgGY0J4YwKLunmWghdOXMIP27799luld9Nh/fr1yv7qPDwLNlvBqW1Z48GzFjwOc9p4AGIFOBVxCIvP+AaWn7Bz2I4LzsXNGZR8a0LwkxBeOHMFP7kfPHiwymbBYu9AjokrRzaLpnnavTTPnWFxtmcebg6J4sxWpXksvvlm0fi+fftU7vFAsEjul19+UQ5DMEeNHTMeCDj7lecshGcoUiTggYkdQz4fHhR5hiWQ4wcAiH8wJoSG64Edjw067Ny5U2U3ZDF1ODPCHMrMYcs8u8z1m3h85QdeAMQK0FTEIa482ePGjfNqdz1pd2U44jAb36fi7dq1U39dqVJd+b1907nylDCHS7355pui4T148GCJzp2dBn66/+qrr3qd2zvvvKPCh6TsTCWFnQW+RpwVK1hlU9aNsD5EKobEht+Vyco18+B53nzOnOo1knB2Ll7efvttlYWLByoOkwMAJCYYE2Q485OES5foG4YbSF/ItaEcDgf95z//Cetz4hkSzmb44YcfqtCniy++WLUBECvgTiMO4eqePGU7adIk5QywdmL58uUqxSxrJvr06aP68WsOkeEUdnyDzQIzvnFmQbRrEOKpZBYUc5wpazi4HgXXhuCFY3h79uypQoNYSM0zCzwbwnoCnr7lp/uhwk/VR48eraae2aByalaeteDz5HjT0g7j4almnarfnCr3n//8p5p14DS6PIBwvC23c9gYP9ni9LzsEHEhpNtuu03F2PL1ZAdM94lXac5WcGgag9AnABIbjAkyrDXjcCW22TwG8gMiThbCs7w83nC7J5xCnG/4+cERp47lMY4TebCt54d2PGaVhu3m9OAMC8cBiCkilmcKlGr6wGBpYBmbzWY8/vjjRqNGjYykpCSjXr16KmWdZ/pXTjXKKUjr16+vUtnVqFHDuOyyy4yVK1d67WvJkiVGx44dVdo93/SyW7ZsMYYMGWLUrFlTHadOnTpqHx9//LHWOfumlPVMIduiRQu1z+zsbGPEiBHG0aNHw0qH6plSNhhS6sCioiLj2WefVcfja1W5cmV1TfgaHz9+3N3vyy+/NM455xwjNTXVaNiwodqG0+D6vkdOKTtgwAC/Y/N74qWkKWVd7Nu3z7BYLMZZZ50VdF9IKQtAbIMxoeT27H//+58xaNAgo0mTJiolO9vtVq1aGf/5z3+MEydOePVlm+taOMV4pUqVjPbt26tUshs2bNA6XrCUsi4KCwvV+JKVlWXk5+cH7IeUsiAaMfH/ytuxAQDos3DhQjXbxEX3eNaEs1f5hjdxKlvOAsWaESk7F89KcYgbP6njEC0WGQIAACg7uLgsz4yMHz9ehaVyVIBvpipOIVu7dm01S8Jhv77wbAqH3d5+++1qRsWVdQqAaACaCgBiFA5l43CxtWvX+q179913VYgWh25JcDtvG2oudgAAAOHBDgHbX07q4Qs/LGJNIodBSbBug7flZCoARBuYqQAgxmCBPedTd9G1a1d3xhEuSLhx40Y1O8GzGZ9++qm4j19//dVdX4NTB3P2LwAAAGVHQUGBKrLnghNquOpgLFu2TNll1lGwOHv16tXiPljXwdmmGJ6h5oQpAEQLcCoAiCN4gOHZBw6LYkGhZ/FBAAAA0cmNN96obDZnYOSZZk6GAkCsAacCAAAAAAAAEBbQVAAAAAAAABACnFa/YcOGlJqaqsKQly9fHjSKgIvn+i6etbc4bxInV+EkK5zOn+t2cRFj37ooXKCYRf6cpGXYsGFRJdaPmZkKLlDG8eFcG4Avdvfu3enZZ5/VKk7jgqtM7927V8WfB6uMDACILdiMcUYrzppiNpuLjWvmCu26cHYWHjRAbAA7D0B8Ek12nmt3sZh+4sSJyqHgQrozZ85UdbVcOhlfZ8DzfA4fPqzqx3CRWg59Y/ielu91uYYYZwljbeS6deuUTtJ1blwcketeceFhm81GN910k6qpMm3aNIoKjBihf//+Krfz+vXrjbVr1xqXXnqpqq9w6tQp7X3s2rXLK9c0FlwDfAfi6zvAv/FgcN73mjUsIe2Ta7AEyxcPogvY+fL/HWLBNYh3O9+lSxevelYOh8OoXbu2MXbsWK3tX375ZaNChQrue1in06nO4fnnn3f3OXbsmKqLxfVUmI0bN/rV/Pr2228Nk8lk7Nmzx4gGYqai9uzZs71es5CJvUHOgnP++edr7cOVIWfhsuqUmXnGy81xZPj1zbFn+bUdcFT0aztsy/RrO2Lz3t8xW5pfnxNF/h7xKZt3vmqmwJak1VZos/i1OYU2h13w7h3+szaG0M8k9CO7f5vJ6d/NZAjbiv3820i3TQfNCSrDrLetYTb0+in75oPQZlj920xSm9n/4lmSHP5tVv9+yUK/VKvdry0tyfspT3qSza9PxeQC/zZrvl9bVpJ/v8rWPL+2qtaTfm3VLP5tNXz65Z5y0uXn7nH/xgPBT4r2H3DQtlUNqGKF4qM/T5x0UqOOO9R2mK2IDVzfgR2rG1JFDzt/0JHr1e+Aw98+5jj87fkhh/936oAwPkhjwXGbv50/5jM+MCcF23+yKMWvLV/ol1/oP4zbBdtvLxKGe2k8KBJsv82kNR6Y/U0EmXzGCJO/qZHHDKnNoddPGh/EfhTG2BLGWCK2WTTbhG2dPl8Lp6pT69OWJIw3qf4X1Jzi35ac5v/BZqX72/Rq6d6/MaZu+jG/toZph/zaWqTs9WtrlXzE6/WpU07q2vlQmdl5ru/EYUUuUlJS1CLtn+89R48e7W7jmZN+/frR0qVLSQeuQcK1SjIyTtuDbdu20f79+9U+XGRlZalZEN4n9+W/HPLUqVMndx/uz8fm7GFXXXUVlTcx41T4wgW7mCpVqgTsw8W9eHHB02YMOxSZHl+0U8Lgkm73b0u1+1+uFOEGP9mnLanIfyBISvJvs9r8v7yWIv/9W4RjWmzCRykMLIatnJwKZ5w6FZZycios/hfPrOlUWJL8R3VLkv93xZrk/UaSfF6rtmQ9pyVFdGT8v7NpQlu61f/cMq3yQKEb1piReXopDkdJv2cgYgSy8+xQVKxw5rtT4PD+zuT5vA40FuQKdl9/LBBsf5Ge7bcmSeOB/7YWS5LWAyWzRdOpsAi23yrYecH2m4WbYN9+ZnsYDgScCtHRIN+vheBUSG2iUyG0WdKE8SHDf39J6f7OR3KG//czNU2w86n+x6iQHFk7X69ePa/2MWPG0GOPPebXn50PrgOVnZ3t1Z6dna1C9IuDtRdccNazuCE7FK59+O7TtY7/+oZWcVphvg929SlvrLEaM3vXXXeptJnB0q5xbNrjjz8e0XMDAEQ/TjLUotMPRDew8wCA0rDzu3bt8pupKAveeecdatOmDXXp0oXijZjM/jRy5Ejl5RVXUZKnpnhGw7XwFwYAAJwh/AeiG9h5AEBp2Hl2KDyXQE4FFye0WCyUk5Pj1Z6Tk0M1a9YM+mHk5uaqe1fO2uSJa7tg++S/rqK1Lux2uxKBF3fcSBFzMxWjRo2iWbNm0aJFi6hu3bpB+waKhzvoSPea+t4vxMdKbYds/nF8B4v859aOFnnHzJ4QpraleNk8YWo7v9B/6tAmaSWkUCdhalsKf5LiZc1iWJMUV0uabVJMUBjxsSV8gCxJO8SQKFE/IbWZtHQWhsWkFf7klKa2hfAnpxCOYAjhHE6r/4fhFD5Hu7Ct3endZhcugNgmvAmn0M8pfhh6WHy+KHkO4UsXBIdhqEWnH4huAtn51YV2yvAIz9vv8A4b2G8TbLxd0E8U+bcds6X7txX5a+eOF/prKnIFOy/Z/iJBA2EvEuy80CbqIookDYRJbzwQtBJSGJMY2uSzrVl7zAhDe1Ha44ihOUaENZYIbcJH6xTu2pw+twnOZP+DOqS2VP82pxCGlF/o32YTvp/5Qhh2nt3/HibfIehCfd8EH8NH9ptXyF8K75vq8rDznCmqY8eONG/ePLryyivdETTz5s1T96jB4AxRHK55/fXXe7Vztid2DHgfXACROXHihNJKjBgxQr3u1q0bHTt2TOk5+PjM/Pnz1bFZexENWGMpldjtt99On332GS1cuFB9AKD0STVbqHJyMpmFm8xwhHPaToURxU6F9uAgOBXSnKCmCE9yKgxBUyFqLwRNhVXSXgj7S7F43yGkCncW6b53DBzHavJP45dF/jd9FQz/G650p39bsjOV7KYT5DSdiZsPF4Q/AQBAfFOWdv6ee+6hoUOHKtE0hzFxStnc3FyV4pXhdLN16tRR4Zm+oU/siFStWtVPJ8Jh/U899RQ1a9bMnVKW0+e6HJeWLVvSxRdfTMOHD1epbDmlLDsxLOLmftGANZZCnjgP7xdffKGU/y5RCqvjuW4FCA++X766VgPqm12HkixmOVuTLpF4uFvGQu2wCOfS6W6r6VWJejZhW7GbT6NJ3M6/zSz0M+v2cwptRQ5yGnY6ZP6ZDiTPKblH6TOIOKCpAACAuKUs7fzAgQPp4MGDqlgd34/y7MLs2bPdQuudO3f61dLgGhY//fQTff/99+I+H3jgAeWY3HrrrWpGomfPnmqfnpkHp06dqhyJvn37qv1fc8019Oqrr1K0EDNOxYQJE9xVCT2ZMmWKu3AIKDnsUFxWtyFVrlqVTEn+05CMeL+rPX0cp8UGdS+KqZRnUqjkzoLuzIzZZ8ZFcgJMwrSURXj/FmFb3xCm023CLAoZVFTgoKSDvYmKiA6kyAY5FDBTAQAA8U1Z23m+uQ8U7rRw4UK/Ni7WHKzeNM9WPPHEE2oJBGd6ippCd7HsVMRI4e+YJM1sUTMU7FCYM9JDuy+OhFNR8nt0nV2FN3tRyk6F7g2/vK1001/yfjpOhVl0DPQcCKtwUEEqQlaTk5JT+YlPZbLlnEuHjB8oXKCpAACA+AZ2PvLEjFNRmhxyZFKeRz7yw1LBI0GUfVgoWuQrymaO+RS2OyWIsk8V+gvzCoWaFJIYylGkK8A2awnzqiSnUpLZrGYo3PeDorZB7+42LPFbGROWY2TS29aQOmo6CybJedYVBAodw7nETqeGpsQkiECEwnylkWjOkmIlMiVTrj2bDpq9C+zlOwSFaBD4DHXyOiH3U+yyoqCRVy2UA0UVS8WeB7LpkgC7oFDXpgsCq0K9wnSSTTdLouwiTbG11E8Sagt6Oh1Bt1T8ziwUhNEXahvlI96WdqerzxP1dIJoWluo7b2tI7n4AnmBhNp2oc0hfJ+k+5Djwj2HTaj5ZRPeWJHwxhw+IsOCfP6CrSddYOcjT0I6FcAbNhcm9R8AUYzJ9V0N30FxaMba6vQBAAAQfcDORx44FQCAhIMfjOpUy0ZFbQAAiE1g5yNPTBa/A6C8WbFkMbWpU5NOHD+uXn8+Yzp1b3lW0G3eeOF5urZfX4pnWlWqR3NnfUfRjjOEBQAAQOwBOx95MFMBYpb/3H0HfTnzI7/2r39cSvXLuI5Ju06dacGaX6lCRe847bLmyKFD9Ppzz9OP8+bS4YOHqGJWFp11diu67d57qH3XLhE9l1jGbpjIpqER4n4AAABiD9j5yJOQTsVRZwYVOKxBRdlSxdSjRf5tUrXsEz5VVPMEAXa+INSWqqU6taulmvWqYgtCOgvfOHEoCFda/qvacqmLrTX7hSKS4749e/ehp154xatdpcUN9xGzJK7zOLckazJVq1bj9PtyLYwzQMYl48x7c5+bKHwOXiTwnmHDyWYroqdeeZXq1q9Phw8domU//kjHDx+VH6vrFKBQxzWK7yfMa0qHdDhN5PCpxk1CQcCSTWWb6ag9nQrN3skVCuyhCbUdZFKLTj8Qm/x6sg4lG2fs7DEf+31MrHYtCLALNatdC1WHDZueAFusYi2IYy2Fgk0XvvoWSWytK9S26bYZmlW2DQ3RtyTU1hRga/bzMuAhFlvVRjd1uCDKFgupCunwnNbiq2X7CrcZe4qwnVBP1Cy02cXK6/7fbbtQ5DZPeDBzwHd84HPWeIBjyxO+xEGAnY88CH8CpfNFKjxB2StepObTzqNWk8+hRl9eRxW2zynzq5ucnELVatTwWiyW08Zu/vez6dpLL6QOzRrQxT260Bsvv0B2j5vP996aSFdd2Js6N29Efbt2oCf/82/Ky811r9+7exeNvOkG6t66OXU+qxFd2fd8WjR/rlq3YulialPvTPiTi3mzv6UBPbtRxyYN6LbBg2j/3j1Bz/+TaVPpb73Oo46NG9Dl5/ek6e9OCdiXj7V62c90938epi49elDtevWoTfv2dMsdd1Cf/v3d/d6fOJGu6d2HujZqTBe170j//feDXu/ri+kzqGfT5vTD93Pob916UtcGjejem4ZTfl4efTn9I7qkQxfq2bQlPTP6YXI4zqRg6d+2K018/mV64JZ/UZe6Tanv2R3pf2+9G/T97du9l+6+cQR1qteautRvQyMGDaPdO3a51//841K6uvcV1Ca7JbWv24b+3u9a2r0z+DUrDVyDjc4CAAAg9oCdjzxwKkD4X6KiU9Ro1mCqvvZNSsrNIbOjgDJyVlGDOSOp6q+Ty+UKr1r2Mz109+10/U230BdzF9GjY5+nLz6eQZPGjztz3iYzjX78Kfp87g/09Euv0vIli+nFp88UnXnq4dFUVFhE7878nD6ds5DuHv0wpaf7p5x0kZ+fT2+NH0f/fWU8vf/Zl3Ty+HG6/1//DNh/1qef0OsvPEd3/PtB+mLhIrrzwdH0+vPP0RcfzRD7p2dkqGX+7NlUVCg8TnK9L7OZ/v3UU/TpDz/Qk6++Qst/+olefuJJv3Od9tbb9OykCfTG9Gm0cskSunvoMPpx7jx67X8f0n/feJVmvvchzflyltd2746fSGe1bkUfLfyObr5zJD0zegwtWbBIPA+bzUbDr7meMjIzaOrsj+l/cz5V53/L1UOoqKhIOXgjrruVuvToSrOWzqaZcz+j624aJM/0lDL8VEx3AQAAEHvAzkeehAx/AqVLlY3TKO3IZnFd9sqX6dhZV5EjpXKZXPYf5s2hzi0au1+f1/sCemni2zRh3Is0bMTtdMW1A1V7vQYNaNS9/6aXnn6S/nX3farthltudW9Xp159uv2+f9MTD/2bHvnvs6pt3549dOGlA+isli3V67oNGgQ9F7vNRg89NZbO6dBBvX5q3Kt0Re/zaN2a1dSm/ek2T9548Xm679HHqN+lA07vv34D2vLH7/TxBx/QFf93+rw9sVqt9OS4V+jx+++jmR+8Ty3btKGO555Ll1x5JZ3VqpW73/W3eryvBvVo1IMP0lMPPED/efYZr3N9+LlnqV6jhup1v8svo1kzP6YFG36l9MwMatL8LOrcszut+GkJXXzVFe7t2nXtTLfcdbqCaMOmTWjtshX0wRtvUfc+5/ud77effkWG00lPjn+erH/NlI+d8AJ1rtealv/4M7Xt0JpOHj9JfS65gBo0Pn1tm7c881mWJZgWBwCA+AZ2PvLAqQBhk7X1m4DrzI5Cqrh9Lh1tfm2ZXOnO3XrQo0+fdgKYtLTTcdObN22gNStX0KTXzsxMOB1OKiwsoPz8PNVv6Y+L6O3XX6VtW/6kU6dOksPuOLM+PZ0G33wLPfXQv2nJoh/o3J7nUb9LL6PmLc/cvEs3/a3btnO/bty0GVXIyqKtf/7h51Tk5eXSru3bacy999Bj99/rbudwo8wK/hofFxcOuIzO79uPVi//mX5dtYp+WrCA3n3jDXrsxRfpb4NOOyI/L1pE77w6nrb9+Sflnjyp9llYUKDCm/h9ManpaW6HgqlavZoKp2KH4kxbdSUM96Rt547er7t0oA8nvCOe6+b1G2nn1u3UqW4LryCiwoJC2rltB/Xq15OuGfx3uunKIdSjz3nUo08PuvyaS6lGzRpU1jjIrJbi+wEAAIhFYOcjT0I6FYfsmZRqPyOeZuGnL0cEofYJobKqryhbEmaLouxCTVG20M8kVUsVhVRSP/9D8K2VW0QctKK2fxMfwWw7E68vYbbll74g7i/S09OpfgOfTE8GUV5uHo285z7qd/HpWQBPUpJTac/OnTTy5hto4PVD6Y77R1NWpUq0esUyevSBe8hWaKO0VKK/DxxMPc7rrXQU7Fi8/fp4uv/hMTT4plvc587vSy1/7Vv920Mk6H5t+JzfqTz1zzHPvUDn+DgcZovZW2jo8zGmpqRSt/N7q+W2u+9Vjskbzz9PVwwcSHt27aLbbxhC/zdkCN3+4IOUVbkSrVm2nMbccw/Zi2xkSjt9/CRrktc5cUG5021nDsblEJ1Op7e+0SAy/hLzq5eG6bTG32M7rijOr3NP5VGrdm3o2UnjyeTzBahStSo5DBM9PeFFuv6fN9GPc3+grz+ZRS8/+SK9+8UH1L7LmWty+ggs0jaRk0x0wp5KBWbvMLRCu/DFDoKhGdrE/UKhYcOGtGPHDr/2f/3rX/T666+L28ycOZMeeeQR2r59OzVr1oyeffZZuvTSSz3OwaAxY8bQW2+9RceOHaMePXrQhAkTVF8XR44codtvv52++uorFf52zTXX0CuvvEKZmd6C9kRiy4lqZHWcEV7n+VS8zitI1qx27e98GoKtNhWWcrVrW9kLsC1FhmZVbEPvXIR+vm0msY9TT4Ct2UZSlW1BqC2Jt0XCSfxh1kyWIYm3rYKgWWpL9m5z+gi3GUuK/3Z2zXsJsXq6WPFcOIZQKbtAuOzej69k7LmBw34jaedBYKCpAGGTV7NT8PXZ3k+3I0HL1m1o25YtVL9hI7+Fb7o2rvtV3TDf//Bj1LZDR2rYuAkdzMnx20+t2nWU4/HKpMk0dPg/6eP/TQ14TNYIbPh1rfs1z4CcOHGcGnvc/LmoVr061ciuSbt37lDpbz0XDoMKhSZnnUX5efnq3xt/+UW9r/see4zaduxIDZvw+9pPpcWvq1Z7v165mhqf5f/+mFZtW9OOLduoSrVq1KBxI6+lQlZFr3633TuSps/9jJq1PIu+mvklxaqAb8WKFbRv3z73MmfO6WQF114rz9QtWbKErrvuOho2bBitWbOGrrzySrWsX7/e3ee5556jV199lSZOnEjLli2jjIwM6t+/PxUUFLj7DB48mDZs2KCON2vWLFq0aBHd6hEGBwAAiQaE2pEHTgUIm8OtbySnxf+pH3OqTk8qqHZ2xK/yiDvvoa8+nUlvjHuB/vz9N6VV+ObLz+nV50/rCuo1bKR0BVPffYd27dxBX346kz6a+r7XPp55/BFa/MMCdePPTghnfOKQpkBYk5Lo6Uf/Q7+uWU0bfv2FHr73TuWwtGnnr6dg/nXv/fTOa+Np6jtv0/atW+j3TZvosxn/o/cmTRT7HztyhIZdew199cnH9PvGjeq8vv/qS5ryxuvu7E/slPD7mvbOO7R7xw76auZM+uj9D6i0YA3F5Fdfp+1/bqHp70yhOV/MosG3DRP7Drj2aqpctQrdPvhmWrlkGe3evpOW/7iU/vvAo7R/zz7atX0nvfjYs7Rm2Sras3M3/TRvEe3Ysp2aNG9CZQ2nptVdQqF69epUs2ZN98I3+E2aNKFevXqJ/Xk24eKLL6b777+fWrZsSU8++SR16NCBXnvtNfcsxbhx4+jhhx+mK664gs455xx6//33ae/evfT555+rPps2baLZs2fT22+/TV27dqWePXvS+PHjafr06aofAAAkImVl50FgEjL8CZQuhVWa065+b1Ctnx6h5Nx97hCYk/UvoD29niuXy92jVx96ffIHNOGVl2jyhNfJmmSlRk2a0jWDBqv1LVqdTQ888jhNnvAavfLs09Sx67l0578fUhmjXDgdDnrqkdGUs3+fCiPhff57zJnsUL6kpaXRzSNG0QO3j6ADOfupQ+eu9OTzLwXsf80/BlNqWhq9O/ENevG/TyidR7MWLbwE5J5w5qQ2HTrQB29Not07tqvsSjVr16FrBl9Pt9x+h+rT/Oyz6f7HHqMpr79Orz79NHXodi7d+dBo+s9f68NlyL9uo42//EoTX3iJMjMr0H1PjaEefXvL1yM9jd77+hN66bGn6c4bbqXcU7mUXSubzu3VkzIrZJKtyExbf99Cn037mI4dOaa0FINvvYGuu/n0Z1SWcBiVU+OZivOv0KsTJ054taekpKglGJzh6sMPP6R77rmHTAFSWi1dulSt94RnIVwOw7Zt22j//v3Ur18/9/qsrCzlPPC2gwYNUn8rVapEnTqdmTHk/jwjxzMbV111VbHvEwAA4o1Q7TwIH5PBj8ISBL4x4AH5jp+uoJTMM7qHg0X+cceHCvzbjhZyQLo3pwr9byzyC701FbYCf9/NWWgt1aJIYptmEaM6aWk0und7ql67Dpk5rr6kxe+cDsrYv4wshcepoGorKqp4Joyn1IvpldgIaIazhBFiKYZnmkp527D2Z+j18/k6XtKxMw2+dThd/8/hZ3YlbifETAvHtIj9/IOXrX+1OQodtG/XPppR8CWZk71rWRSdstGkXjPp+PHjVDFIlXOXDfjy1yaUUUHQMPmQe9JBfztni187axwee+yxoNt+9NFH9I9//IN27txJtWvXFvskJyfTe++9p0KgXLzxxhv0+OOPU05OjgqPYg0FzzjUqlXL3ef//u//lKMyY8YMevrpp9U+Nm/2zsBWo0YNtZ8RI0ZQIuH6jBu//xBZ0lMD6thKW8NmFgrTaReSE/QJlrC2NcLQVBSvizh9XKfWtiaHdz+zTdJPCG2CzkJVwPTt5xT6iW1UujqLcJA0FRbBmP5Vc8mrn6ip8L6fcKb4b+dIldr892VL9z8PW7qglRCyq9uENrFfBf8Pw8jyFwKlVSjwPt+8Avrj+mfKzM4Xt19QPJipAKWH2UK5tbvjioKoR3fK2/HXDcauXbu8BpviZimYd955hy655JKADgUAAIDosfMgfOBUAAASdFq8+OkeVx92KEJ5gsUZoObOnUuffvpp0H6su+AZCU/4Nbe71rvaPGcq+HW7du3cfQ4cOOCXNIAzQrm2BwCARCNUOw/CB+oUAIAW365e7hX6FMtwnK1DY9GJx5WYMmWKCj8aMMA/pbEn3bp1o3nz5nm1cQYnbmcaNWqkHAPPPjy1z1oJVx/+y6lmV61a5e4zf/58lQWMtRcAAJCIlLWdB/5gpgIAkHCU5bQ438yzUzF06FBVENGTIUOGUJ06dWjs2LHq9Z133qkyQ7344ovKAeGMTStXrqRJkyap9aybuOuuu+ipp55SdSnYyeCaFhxSxalnGc4axRmkhg8frtLOsoB/1KhRSsSN0CsAQKKC8KfIk5BOxUl7ChXaz6RAPWHzF2CfLPKPmfYtnsQU+IiyGZuPCFsWBAqFkgRBoCQStIQjCJTa+PRcRdyCFL8LT2xtlHx/2sco4YbSzKek39PVeEsdhSbNbvrnork/zszl308Qb/tq6aTiTNLn6hT6CffvgjaTyOzf0VVjycE5OgwT5dmTyWn2LjpZZA/tSRM/mSqrrCAc9sTi7JtvvtlvHbdzViYX3bt3p2nTpqmUsQ899JByHDjzU+vWrd19HnjgAcrNzVV1J3hGglPGcgrZ1NQz12Dq1KnKkejbt6+7+B3Xtkhkio6nkNnDjvsWpzPbJBtMpZoEIywBttgmJDwQjyv1K7nYWhJXi4Jru39FNJPNUbwA2663L1GALRkSp7Stpihb90FCOHH4glDbJNg+sgjJW5L87zlMKd73HGafAryn2/xv98x2/zaTU0hWIN4PlDyDiCG8V5vZ/1zyTd73Yc58I2rsPJBJSKcCeOM0+NaQSyXjhwWiGOdpJ8YZVnlbclfn5kWnX6hcdNFFqr6ExMKFC/3auDBeoOJ4rtmKJ554Qi2BqFKlinJOAAAAlL2dBzJwKgAdyi2k43mFlHn8CKVXqEQmTmMXzkwFRfNMRSmnZw1jh9GUelYrzaz0wEfzQ5RmKkzCtiYhzax6jmR30qkjJ+ikI4+OG7mUReFhM6xkM4pPNWjDYAMAADEJ7HzkgVMByOE0aNzC9XRN24bUslYeWaVp2BBCc7SJtYmR0n6YIYVEhXPccPZnKr2aFybNfkI0leho8AS2w3DSVttemm9bo8KgwsUl0Cu+X6x9SQEAADCw85EHTgVQHMsvosk//06ZKUmUnmwlixCSapaKB/nXq5HjL6WiRdL9mu4MSRnPVEhP1qWbalHrK2kPhH5O4dcnhLOKMwTitkKbkaR3DGey/wV1+m6bJFz0ZCGGOsn/i5KU7P9FSRHa0pP9g9ArJhdQvlFI+VRYah+7U3PKO3z3BQAAQHlQ1nb+9ddfp+eff572799Pbdu2pfHjx1OXLl0C9mdN3H/+8x+VapxTfjdo0IDGjRtHl156qfY+CwoK6N5771VJPQoLC6l///6qYGp2djZFAwnpVJywp1GSzUOoLYmyPda7KBCETjahzU+YLVRpFcV/hbrVs0mzWqrQzx582/xCG+WTTXQWzIJjIO3PJDgkJkEkJxRPlrfVuZOUYtjFR+aazoJ0cy/0cwpVUEWHRLqRt5pK7ixo9nP6f439nYUAbY4Uo1jHwxDaKNX/Q7QIostkh/+XNt3w/9JWMAr/+teZ36nVx8O1lZlQG6kGYxXrCauXONXXloYjthb72TVtsG61a81+oohaFGoLYmhdAbbYJjxQsPtfBFORTz+HYOSF7UgSajv0xNuG2K+UBdiSaDycsUmIEDD5ZI9TWKWq796G3iTc05g8ktOcObdw7rD9z9eQKoXrPrQT3r/d4vP+861RY+dnzJhB99xzj8q4x6m72Tno378/bd68WaUT96WoqIguvPBCte7jjz9WWQC5nlGlSpVC2ufdd99NX3/9Nc2cOVNVDecEHVdffTUtXryYogGMmACAhE01qLMAAACIPcrSzr/00ksqjfdNN91ErVq1Uo5Aeno6TZ48WezP7Tw7wdn9evToQQ0bNlTpxHk2Qnefx48fp3feeUf1u+CCC6hjx44qffmSJUvo559/pmgAIyYAIGErreosAAAA4t/Oc2FRz4XDiyR41oGLjfbr18/dxqm8+/XrR0uXLhW3+fLLL1Wh0pEjR6pQJU4b/vTTT5Pjr1k1nX3yeq5D5NmnRYsWVL9+/YDHjTRwKgAACQdmKgAAIL4J1c7Xq1dPhRS5FleRUl8OHTqknAFfHUN2drbSQkhs3bpVhT3xdt98840qYspFT7mwqe4++W9ycrJXyFRxx400CampAAAkNvpZQfDcBQAAEsHO79q1iypWrOhuT0nx16aUFKfTqXQRkyZNIovFokKX9uzZo0TZY8aMoXghIZ2KU7YUL6G2KMq2CaJsoc1RJHxhfSq3SlWxpTZdUbYo/tMVbwttoijbrifKlsTbuiJvSagtidpKnv1JU3AnCsn0BNhS9l0x01OS8B2QromPvrB18m5qZsmho850+rHgLLL99ZMVNYK610k7hVLJsmZIgjuHsKsiKX2sUKfCIvRL8klPZhdErcFwGia16PQDsYn1pIksNlNAG6lfxVqzTbCZcvVsSYCtWwFbssuaAmyxenbxFbAVYj9NcbXNViJRtii2FrY1JGMoiaO1hdolz/lmiFW7g+/PRAZ1aHyIalYqoH1H02nN9mpk/GV7Vc0o3/7JQkVt32sgJUYRjl18pZ6/0M6GKI2bJU9mYlh97qUKzGVq59mh8HQqAlGtWjXlGOTk5Hi15+TkUM2aNcVtatWqRUlJSWo7Fy1btlQzDBz6pLNP/st9OYuU52xFsONGmoR0KgCIdmpYTtBzVT+i1il73G1HHen0+PG/0aLCFuV6bvGAU/MJFrI/AQDKima1jtPDV62hWpXz3W17jqTTk592oK0Hwi3xCcrKznMIEs80zJs3j6688srT+3A61WvOxiTB4uxp06apfqyVYH7//XflbPD+mOL2yevZMeG2a665RrVxZqidO3cqvUY0gLl9AKIOg16uNs3LoWAqW/LoucozqYnV+0kGCB2nYdZeAACgtKmQWkRjB63wciiYOlXyaOx1yyk9WZjmAlFj5zn161tvvUXvvfcebdq0iUaMGEG5ubkqcxMzZMgQGj16tLs/r+fsT3feeadyJjgtLAu1Wbitu0/WeQwbNkz1W7BggRJu8zp2KM4999yo+HZgpgKAKKNrylZqniw7DskmBw1KX05P5l8e8fOKJxxkUotOPwAAKG0uaruHKqbLjkPljCLq22YPzVrbBBc+Su38wIED6eDBg/Too4+qEKZ27drR7Nmz3UJrnj1wzUi4RODfffedqjNxzjnnqDoV7GD8+9//1t4n8/LLL6v98kyFZ/G7aAFOBQBRxtnJe4pf7/1wC4SI7tMpzFQAAMqC5rWPBV3fotYxmrUW1z6a7TyHJQUKd1q4cKFfG88oFFdPItg+mdTUVFV1m5doJCGdilxbElk9xNn5Nn/hU6Egyrb7VsrmQBWb/5fR7CPelgTYFkmUrVsVu7B0q7mKYkJJqC2JvKUq27qibLFf6Qi1qyWfpGSTnfYVVnKL3k7vTNiV9JDCpCsuM2m1iaJsSbztNFGuLZWCccqRShbhOyBWKA9NvxwUqVqqdD0dglBbusZOoZ9NECYWCm35du/frF0QqwaDJaB6T7BArJJ00tsG+tpIXQG2KJi2l1yALdtWPQG2JLaWq2KHIcAWxdb24gXYmiJsQxJbS/tyFFcp26CaWfnkcBh08GQalQiTUMVaEBaHhXAMFm/nFvjfd3hyqsAaQKwuZb3wETSLFbuF8UZoM1v0tnVqjn1OizAeWIV+wt2o7/4cBaF9NrDzkSchnQoQv5xTcSfd1mA+taqwV73eV5BFU3d3p68PtKdYYV7u2XRnte9VqJPE7Nw2ET+neAMzFQDELhecvYcG9/yD6lXNVa//2F+RJi84i1ZtrU6xwvz1tWlAh12B12+oHdHziUdg5yMPVIggbmiZuYeeb/U/t0PB1Eo9Tvc1/ZaurrmCYoWjjgx67fCZipmerMhvRF+fahvxc4o3UPwOgNjkwja76cEr1rodCqZZzRP01MBV1L7hIYoV1u2qQt+urSuu+2pVfdq817vAGQgd2PnIg5kKEDcMqfcTJZvlp/s31PuJvspp767zEO3MOH4ubS+qTgOzllHTlByVTvbr3Lb0yclOZNfPLg4CYDcsZJMSo/v1K3nOegBA6WI2GTTk/N/FdRYzr/tD1XmIFV7+ujWt21mFLm2/i7Kz8mj/sXSatbo+LcAsRakAOx95YuMOC4BiMJOTOlfaGnB9paR8alVhD/1yskHMXMtl+U3UIsWagvBA8TsAYo+G1U9QdlbgLBVn1ztGmak2OlWMXiF6MNHcdXXUAkof2PnIk5C3KwX2JLJ6CD1lUbZVS5RtEipqm3yEVGaPqq5n2jSrYheVXJRtkaqvimJwTTGh0GaS2jSF2nI/qSJp8eJts6gE98Zic5K5UJjJ0BQgi6JsTQGbSRCmSacsfbYO/4LvsvC9jNOfhiNod0pVVQUBtl2oqF1o9r9BsFq8L4AjZKG2XlEknT4gOknK9U5C4Su4FsXWmgkqtO2jKKIOoyq2rgBbEPiaijQF2JLYWre6tcb+pD6iIFmaJZSqZ4cjwJZE1IKwWD6EcAy9s5OrgGsiHdf/AIaW8F2qZG4q8v98pMreFkE0LY2RVlHQ7X8qTmGMNHxvw4QEJcGAnY88GDFB3AiyVh5pFHD9saI02ngcwjfg+r6YtBcAQHSw7UAFyjkWODvehl2VYmiWApQ1sPORB04FiBs+3N6dihyWgOtsfo89QKLiVAFzekuo7Nmzh66//nqqWrUqpaWlUZs2bWjlypVBt+Gc5h06dKCUlBRq2rQpvfvuu359OC95w4YNVZ7yrl270vLly73WFxQUqOqsfNzMzExVHCknB9XXQXzdJL7/Q1NxncNpovcXNYv4OYHEtPNABlcSxA2bTtShB3/9P9p0vJa7bV9+Fr30W3/6fE+ncj03EF04DJP2EgpHjx6lHj16UFJSEn377be0ceNGevHFF6ly5coBt9m2bRsNGDCA+vTpQ2vXrqW77rqLbrnlFlV91cWMGTPonnvuoTFjxtDq1aupbdu2qpLqgQMH3H24UutXX31FM2fOpB9++IH27t1LV199dQmvEADRyfe/1qWxn51Duw+nu9s4pewjMzrGlEgbxK6dB4FJyEe3RQ4rOexn3rrN7v902yHoJ0jSVAiFaHyL3enqJ+RidXr9JF2EuD9N/YRU8EnST4ixwHbNonaCpoLE4nd6OgtmfU5tujPnH1Qt5SQlW+y0L+908Tsz2QNrAEpZU2GSiv04pIJ4gh5DKIgniRl0i9rJ/TQNqE83seioeJ00r52gNXJKOgvhO1bko4NySHHl5SDge/bZZ6levXo0ZcoUd1ujRoHD8piJEyeqPux8MC1btqSffvqJXn75ZeU4MC+99BINHz6cbrrpJvc2X3/9NU2ePJkefPBBOn78OL3zzjs0bdo0uuCCC1QfPgfeF1dwPffccynRSMp1ktWjWJyvLdXVRUgaCLHop2gL9eyjpJUgcVsh3l3SE+lqJcRCdzY9zYOkjdDQYxiibk5PY+CK7V+wqR4t2FSXalbKV0+ZD544XfzO5DIfgh2RbL+kFSBJs6BZTE6X0r6FNflqQ6TzlXBofmeF76dvkV/GkuTUGtOchdK20ngQnqYCQu3Ig5kKEJccKqxAe/Mqe1fTBuAvDMPsLowUbOF+zIkTJ7yWwkJ5dPvyyy+pU6dOdO2111KNGjWoffv29NZbbwW97kuXLqV+/bzrkrAzwe1MUVERrVq1yquP2WxWr119eL3NZvPq06JFC6pfv767DwDxhUmlYHU5FACEa+dB+OBKAgASDgeZtBeGZx+ysrLcy9ixY8X9bt26lSZMmEDNmjVT4UsjRoygO+64g957772A57J//37Kzs72auPX7Lzk5+fToUOHyOFwiH14W9c+kpOTqVKlSgH7AABAIhGqnQfhk5DhTwCAxIYjMPTCn07/3bVrF1WsWNHdzoJqsb/TqWYqnn76afWaZyrWr1+vwpWGDh1aWqcPAACglO08CB84FQCAhMM17a3Tj2GHwtOpCEStWrWoVatWXm2sa/jkk08CblOzZk2/LE38mo/H2aMsFotapD68rWsfHCZ17Ngxr9kKzz4AAJBIhGrnQYI5FYsWLaLnn39exQ/v27ePPvvsM7ryyitD3g8Xu7N4CD0dNkGsZZeE2lIRu+IL24lCbbGgkn+bRdxWs0CTJN6WtvUQM545F+kYTr0CdpLQSxKESUJtXVF2yWsH+Z+HZE8EsZ4s1Bb6Wf3P13AKIn+nVBBP6Ce8fV2htiQJlIvY+Tf52lnJ7kpt0m9CEm87xGvs/8YcgpjSZvUtfhfaoOAkk1p0+oUCZ37avHmzV9vvv/9ODRoEruTerVs3+uabb7za5syZo9oZDmvq2LEjzZs3z23veEaEX48aNUq95vWccYrbOJUsw+exc+dO934SjeRTTrJ6CEdNvsXvHBGwcZribbFYndRPU1gt9hNF1E4tsbUhHUMqpiYIrkVhdkmL1YUjwBZF2Zrbisk8NG2DTrG60iac8xWTqkjfT4vWvYTFqmfnndK46VMQzyQknykPOw8CE1PuWW5urkqlyPnaAQAg2lINclpXzrbE4U9//vmnysY0adIkVT/CxejRo2nIkCHu1//85z+VFuOBBx6g3377jd544w366KOP1L5ccDpZFnyzNmPTpk1Kq8H20JUNinUew4YNU/0WLFigHrzwOnYoEjHzEwAAIKVs5ImpmYpLLrlELQAAEI3T4p07d1YzqOw4PPHEEypV7Lhx42jw4MHuPjzLyjMILrgPp4dlJ+KVV16hunXr0ttvv+1OJ8sMHDiQDh48SI8++qgSXrdr145mz57tJd7mFLScFYpnKjg7FW/PDgoAACQiCH+KPDHlVIQKD6yeqR85mwoAAKhpcaNspsUvu+wytQRCqpbdu3dvWrNmTdD9cqiTK9xJgitt8yxuos3kws4DACJt50EchD+FCqd99EwDyWkhAQCA65c4NRbUOYl+YOcBABKw85EnrmcqOASBY4w9ZyrYsbDZzeT0EBnJ1bMFAbZQPdsk9iuZKFsWdEtia82K2rqibFH4LfUruThRrA7rlIR+gqhPEnT77kvYTqyULaEp1JbaTBZJmKgnTJP6yQJ0Pd9fO/xfFGpLIvTi9y+KtyWdo/Q7Efo5BcG1IXz+dp9+0nbBQKXV+LfzSafsZLXaAwquw0oooWvPRKG2IKIWt9WsgC31E/ZnaFbZFqtnC21aAmwBUYAtZcvQFVGHJd7WrJ6tK96W0B6HyvhpuW5VcM0xWBRvC/dSsnjbpHUP4/StMSr0CQbsfOSJa6eCc8kHyicPAEhc7E4LmZwWrX4guoGdBwBIwM5Hnrh2KgAAQAKpBgEAIL6BnY88MeVUnDp1SqVpdLFt2zZau3YtValSherXr1+u5wYAiB0wLQ4AAPEN7HzkiSmnYuXKldSnTx/3a1cc7dChQ8WMKgAAIIHBBgAA4hvY+cgTU04Fp100JBFRiChRp6ew0yGIlSRRttAmCa5N9uCvQ2mTBd2SsLqURdlFkghLEjFqVoIVhINyvzCqbPvuv9gewYR5ekJtEqpsk8M/Dt8kVAuV3qq+3Fizp4YA+3SbICT3eW+SANuwa7Zpbiv97gyhzVeYDaE28MWSZyOL1RLY3miKT8V+krBaqjAt9pPE1iUXb4vCapuuAFuzKra0rS6+IuxwRNmalaJLXZQtJu7QrFotvd/yEGpL6F5PabwVxm+zWRD0C6Jso8i/zVog2HnfbUOtqG1oppQNscipC07f/fzzz6vaQVyYefz48dSlSxexLz/4dhUr9dSDFRQUuF/z/e2YMWNUodNjx45Rjx49aMKECdSsWTN3nyNHjtDtt99OX331lbsuEdc3yszMpGggrlPKAgBAsMFGZwEAABB7lKWdnzFjhoqWYSdg9erVyqno378/HThwIOA2FStWVMVPXcuOHTu81j/33HP06quv0sSJE2nZsmWUkZGh9unpeHAh1Q0bNtCcOXNo1qxZtGjRIrr11lspWoBTAQBIOPi5m16dCgAAALFIWdr5l156iYYPH65mH1q1aqUcgfT0dJo8eXLAbUwmE9WsWdO9ZGdnnzlXw6Bx48bRww8/TFdccQWdc8459P7779PevXvp888/V302bdpEs2fPprfffpu6du1KPXv2VLMj06dPV/2iATgVAICEAzMVAAAQ34Rq57nGjedSWOhbKOM0RUVFtGrVKurXr5+7jUOR+vXrR0uXLg2abKhBgwaqjg47Djzj4Jl4iMOoPPfJRZvZeXDtk/9WqlSJOnXq5O7D/fnYPLMRDcCpAAAkHHAqAAAgvgnVzvPNPt/Iu5axY8eK+z106BA5HA6vmQYmOztbOQYSzZs3V7MYX3zxBX344YfkdDqpe/futHv3brXetV2wffLfGjVqeK23Wq0qA2qg40aamBJqlxa+Qm1DqMZrdugJtU2Cfs2vorZGn4AVsHWF2nbNtnBE2TYHmU1OqpBUQHn2ZLI5rfpCbV0hoqRedmqIBHWru+qK64SiZyZJrCdVxRaF5f77M0tVwCVFs4R0WEEQaJYE2KJQWxDJ+ZyKSdJNmjWTEAiWRkx8IIhinTpCbQcqagOf71JeEZk9kg34VcbWFlbriZnDEmpLFbB1hdVStetwqmIbTkqyOCg9xU4n85PVDZfUT7cyttivxFWnNQXTunZe2p9u5enyqrxdHkhjmmYFeemew7AI/YSkJ1ZfobZw71OaQu1du3Yp3YOL0iye3K1bN7W4YIeiZcuW9Oabb9KTTz5J8UJCOhUgdCwmBw1uvpQGNFxHlVPzKM+eRPN3taB3159Lp2ypuKQgpkCqQQD8qZBaRDf1/o36tt5LackOOnIqmb5eXZ+m/tiYHNIDFADiyM6zQ+HpVASiWrVqZLFYKCcnx6s9JydHaSV0SEpKovbt27trr7m2433UqlXLa5/t2rVz9/EVgtvtdpURSve4ZQ2sBNDioXZf0/UtlimHgkm32uiyRuvomfM+o2RpOgWAKMbgp6+aCwCJQIrVQc8N/pku67BLORRMlcwiuuH8P2n0Vb+W9+kBEDV2Pjk5mTp27Ejz5s1zt3E407x587xmI4LB4VPr1q1zOxCNGjVSjoHnPlnXwVoJ1z75L6eaZT2Hi/nz56tjs/YiGoBTAYqlRaW9dF6tP8R1TSsdoj71NuMqgphCJyOIawEgEejXZjc1yT4pruvVaj+dVet4xM8JgGi185xOlutJvPfeeyor04gRIyg3N9ddi2LIkCE0evRod/8nnniCvv/+e9q6datKQXv99derlLK33HKLOzPUXXfdRU899RR9+eWXyuHgfdSuXZuuvPJK1YfDpS6++GKVdWr58uW0ePFiGjVqFA0aNEj1iwYQ/gSKpXv26em5QHSrvZW+23E2riSIGRD+BIA33Zp5h3L40qNFDv2+LwuXDcQMZWnnBw4cSAcPHqRHH31UiaQ5RGn27NluofXOnTtVViYXR48eVc4A961cubKa6ViyZIlKR+vigQceUI4J153gGQlOGcv7TE09E2I+depU5Uj07dvXXfyOa1tECwnpVBhOExkeQmyTJMoW2kTRdAkraovbOcIQZetWyhYF2LIo20WSEVwobSWHV39RlC1UgpUFkJqCMJ0+kgjPoSmQk85NqsgqCbqFNkMSKgunIlYzFdv8NzWLwkFNobZURNYSvMJ22FW2hWvilKrbS21OU/DXxaA75Y3wp9jFlFforRv2TfggJoUIwyZJ/QQ7YoQh8g6rKnYAUbYLqzm4nbUISR/KlEgIlzUrSsvbaoq8ddvKo6K2boITAb/EB6cb/dtswr2U8F4tGpW3DeFepTztPN/c8yKxcOFCr9cvv/yyWoLBYz3PaPASCM70NG3aNIpWEP4EimXlgQZB16/aXx9XEcQUSCkLgDertlULeklWbQm+HoBoA3Y+8sCpAMWy+mB9Wn9YjtfLya1Ac7a1wFUEMYXTaVbZbIpbuB8AicC3a+vRgeNyJr9fd1SmNdurRvycAAgH2PnIgxETaGCiR36+gubtaE5Ff9UD4FnTlfvr0wPzr6BcW+nlcgYgEhh/RasUu+DjAAnCqYIkuu/DrrRqS1V3VEyR3UTf/1KbHpnesbxPD4CQgZ2PPAmpqVDx154x2JpF7UraJhW/k+IRtQvYae5PbhP0E1IBO5+2fIeVXvi5L01K7k41M0/Qkfx0OpRfgcjGwfL2kukndAtDiTHNOrd7kkgljJhXQSshvi+rcN2lQnekp7OQNBVi+K3QKOknPMVjLpyCXsJsLb4GoSkibULxO582T42UDpztg//T6RcKjz32GD3++ON+lVR/++23gNvMnDmTHnnkEdq+fTs1a9aMnn32Wbr00kvd6w3DoDFjxqhMIyze69GjB02YMEH1dcF5ym+//Xb66quv3OK9V155hTIzMylhyS8g8tQJ+NobzXhyUQMh6hNKrr0wtLUcwraizdSLPffVWew7kkYPTutM1SvmU5XMQtp3NJ1O5CdTuSC9/1gknGJ6AoZGP2m80X6sLGqNDL1tpXsOT72la1PhPUiaPd+CeGapsG452HkQGMxUgJA4UZRGvx/JPu1QABCjlGWdirPPPpv27dvnXn766aeAfTn7x3XXXUfDhg2jNWvWqNSBvKxfv97d57nnnlPZPSZOnKhylmdkZFD//v2poKDA3Wfw4MG0YcMGmjNnDs2aNYsWLVqkMogAUBIOnkijzXsrlZ9DAUApgHpEkQdOBQAg4ShLAZ/ValVFjFwLV18NBM8mcN7x+++/X+Ugf/LJJ6lDhw702muvuWcpxo0bRw8//DBdccUVdM4559D7779Pe/fupc8//1z14RzpnHbw7bffVgWQOA3h+PHjafr06aofAAAkIhBqRx44FQCAhENLT/HX4qps6rkUFhYG3Pcff/yhChE1btxYzSBwvvJALF26lPr16+fVxrMQ3M5s27ZN5TX37JOVlaWcB1cf/lupUiXq1KmTuw/35zAontkAAIBEJFQ7D8IHTgUAIOEIdVq8Xr166mbetYwdO1bcL9/sv/vuu2rmgHUP7BScd955dPKkXKmYHQZXsSQX/JrbXetdbcH61KhRw2+2hPOZu/oAAECigfCnyJOQQm0WdXoKO6WaP2JBPLGff5vv/sQ+Dt2CeILwSRR0CwLscNoEcZV2UTupnyQmFPqFJYosKVJxOYtF75hSP+3D6gmrTULxIOlxgOmvzFxe3YTvjyF9fwRxua8wW/rOSgJpSXMqCrC1f3dGsf2k7UqzKNKuXbuoYsWK7vaUFDnj2SWXXOL+N4cqsZPRoEED+uijj5RuAkQOo7CQDJMR+IupaUNEEbXYUdMmFVOE7kyT5v40RdkSpvIouCahKV6XbKacaEMqzKY5joifY/lcJ1GUbdYr8KuD3EvzeyclfQljrPYVZTMWn4KzRohCbRQ5jTwJ6VQAABIbjrU1aTgVLk0FOxSeToUuHJZ01lln0Z9//imuZ81FTk6OVxu/5nbXeldbrVq1vPq0a9fO3efAgQNe+7Db7SojlGt7AABINEK18yB8EP4EAEg4IhVre+rUKdqyZYuXQ+BJt27daN68eV5tnMGJ25lGjRopx8CzD2s6WCvh6sN/OdXsqlWr3H3mz59PTqdTzZQAAEAiAk1F5MFMBQAgQQcbnfCn0PZ733330eWXX65CnjjzEteXsFgsKm0sM2TIEKpTp45bk3HnnXdSr1696MUXX6QBAwaojE0rV66kSZMmucM97rrrLnrqqadUXQp2MrimBQvBOfUsw1mjOIPU8OHDVdpZm81Go0aNokGDBql+AACQiJSVnQeBgVMBAEg4yirWdvfu3cqBOHz4MFWvXl2ld/3555/VvxnOBOVZfLB79+40bdo0lTL2oYceUo4Dp4pt3bq1u88DDzxAubm5qu4Ez0jwPlkInpqa6u4zdepU5Uj07dvXXfyOa1sAAECiAk1F5ElIp8LkNKnF87WOsJQkYamkG9IQamsLV52lWz2b7Jpt4raaomzNitpiJVjNitra4kkfTEI1aUnox/UB/BvNYVQuFW5OJVGyUN1dzCQgiLJFwb0gftP9Tpl9zsUpWQvd76zG7yRwP1PxbZrCRBf8bnU+uVAfYPFMQzAWLlzo13bttdeqJRA8W/HEE0+oJRCc6YmdE3AGI7+ADNGQlw2REFZHPaLg3NtWmSR75tNHITRJY4b0y9eWWusKv6XPVjRW5ugQfouVssPYn261eAlJcC90E6ts+4zXZul+oxzsPAhMQjoVAIDEBk+wAAAgvoGdjzxwKgAAiQceYQEAQHwDOx9x4FQAABIPTU0F9wMAABCDwM5HnIR0KjKdBdQv70/KMIpoRXI92krI5Q5AIqGbLhZZQWKXKhn5dG7Tferfq7ZWp8On0sr7lAAAEQR2PvIkpFMxL2cC1Uo5I/iZY21O96RfRbmmlBKIpovvJ1YE1m1zlHKbJJqSBFdOTfG2KLbWFGULgm5RgC2KxEomrTKk92DWrKgt7U9o06/6KomyBYWddE0kUbZV2lb3u1e8oFv7NxGBNj/RYYgiRBaP+gpIA/UDscmbtyyiKumn/213mOirVfVownctyQhRJKtbdVpbqB0G2hWwTRH43uoKzn36GZLWVqh2LRYtk+yyZlVsQzhfye6JNlj36YLUT7LzsYbuGOx0lHh/onjbJ9GI2SEkgQl2GNj5iJOQI2YGeX8xL7Rvpmfyvyy38wEARBYURYp/rBbD699XddlJ1/XcWq7nBACIHLDzkSchnQqJi22bqI7zWHmfBgAgkgI+nQXEDVd23kFWKaUpACD+gJ2POAkZ/iRhIYPaOPbSHnOliB63eco+GlL5Jzo3Yws5DRMtPnEWvX+wJ+0srBbR8wAgkUCqwcSkcmYRZVfKpz1HMiJ63M5NDtK13bZRy7rHKK/QSgvW16LpixvTsbwzIbcAgNIFdj7ywKnw4ITpTIXaSNAudQeNqzOVUs1nwrEurryOelT8nW7fOoT+LICAHIAyA7MQCQeHgecWRHbYu6TdLrrrsg1u6VZqUhFdc+4OOvesg3TnlHPpeF5yRM8HgIQCdj6iwKn4i32mCrTM3PCMXky3gKZGP0GDptruqv69l0PhooKlkP6ZPZ/u3/aPAMJVXdFUKQuwNfenWynb0D0XsUpr6VkKSRAYVg1UXUW/JAi0OLUE9+QRL+7CcJRyQgCfftJ10hV9l3blbb9UryGmfsUTrMRkzdaqIc8OhGNrUqwOuvXCzVIuCKpTJY/+fu42emd+81IXZWtvG841kMTgGuJtaV9SlW0pSYKJhHEkLFstvAdp/JLE1tJYJe1PugGQrqePKDkiaIrcZaG2XpIWcVupn/S98LnuplCF2popZbXSiwMtoKkgoiIy06OpA8gZiYwZf1En6Qi1SD2d7lCiS4UtlGkuiNj5AJBQINY24TiRl0QT57SI6DE7Nz1ImamBb4R6nx14DAAARLedf/3116lhw4aUmppKXbt2peXLl2ttN336dOUwXXnlld6naxj06KOPUq1atSgtLY369etHf/zxh1efI0eO0ODBg6lixYpUqVIlGjZsGJ06dYqihYR0Kl5P705/WqrSHnNF+iL1bLo2YxjNT5KfFpUVaaaioOv5QVOK2Rax8wEgsTCFsIBYZNaqerTvaBrtP5ZGn69oQP96uxttP1ghoueQmhw8xWZqkmYKTgBAVNn5GTNm0D333ENjxoyh1atXU9u2bal///504MCBoNtt376d7rvvPjrvvPP81j333HP06quv0sSJE2nZsmWUkZGh9llQcOYBMzsUGzZsoDlz5tCsWbNo0aJFdOutt1K0kJDhTxMyetKbaf3cry35kfetdtiq0VF7OlW25onrdxVWocP2TCUgBwCUMrpPp/Dzi1nent+C3l2QFNE6Er5s2FVZRXUEikZav6typE8JgMQhRDt/4sQJr+aUlBS1SLz00ks0fPhwuummm9RrdgS+/vprmjx5Mj344IPiNg6HQzkFjz/+OP3444907Ngxr1mKcePG0cMPP0xXXHGFanv//fcpOzubPv/8cxo0aBBt2rSJZs+eTStWrKBOnTqpPuPHj6dLL72UXnjhBapduzaVNwk5UxEN2AwrzTjWNeD6/x3ohqekAJQVCH8CEWDf0XT6YaOccIML8s1c2gifAwBRYufr1atHWVlZ7mXs2LHibouKimjVqlUqPMmF2WxWr5cuXRrwdJ544gmqUaOGClnyZdu2bbR//36vffI5cFiVa5/8l0OeXA4Fw/352DyzEQ0k5EyFn/caQEgtbufbj4rfNpBul1PHplERDayyzC3YznUk0/s5Pemrw+1PH1BTNGUShVS6olcjDPG2ZjXTECpvV0wrokvb76QODQ+R3WmixZuzae6vdajQHrjKte5TSEnAGJYgUBDwiZVbDc0qrU5dUZt0DD0BtvwdKL5NFGVLAjfN34nuLECgRAfF9QkKn7eOOA8CvpjFZLGQyWQJ/CXRrQitSSAb9OKXbcjhNFGfs/eTxXy6z+GTKfTa7Ja0cXflwMJqTY1fONvqIgupw5v5qVkpj/7WaSc1r32cThYk0fx1temn37JVavXTCGOGlCxC2Le2rRbGJfF6aguaJaFyGPuTBOLS19b34y7tUiy645Jd0A9JCVkkpPHL4v3GTE5bmdr5Xbt2Ka2Ci0CzFIcOHVKzDjyL4El2djb99ttv4jY//fQTvfPOO7R27VpxPTsUrn347tO1jv+yU+KJ1WqlKlWquPuUN4npVEQNJnrzYF+adrg7tU/fQYaNaM2phpTvTNwUg3WrnKLnB/9MVSsUuts6NT5El3XYSfd/2JVOFZwJZwAg3EqrOv0ACAd+GPLs521p8vyz1A0016lYu70KOaUHDQlCh0aH6PGBqyk16cwNZ4/mB2jx5hr0xMx2CX1tQPnZeXYoPJ2K0uLkyZN0ww030FtvvUXVqpVPDbIvv/wy5G0uvPBCJRgPBTgVUcBJZxotOtWCLIWo9HrnJeu8HAoXTbJP0o29fqfXvju7XD4jEGdAUwEizMETaWpJdLii+b+v/NXLofB0LC5pv5u+Xl2/XM4NxBllZOfZMbBYLJSTk+PVnpOTQzVr+oc7btmyRQm0L7/8cneb868ZHJ5p2Lx5s3s73gdnf/LcZ7t27dS/uY+vENxut6uMUNJxPfHNNFUcPLPHmacaN24c0nZ4HACihlqVcqltgyMB1/dts4fMYiEDAKhk0+I6CwCg1OjS7CBVyQyc/bB/uz242iCq7XxycjJ17NiR5s2b5+UkzJs3j7p1Yz2sNy1atKB169ap0CfX8re//Y369Omj/s1ajkaNGinHwHOfLBxnrYRrn/yXxd2s53Axf/58dWzWXhQHh0hxX50lPT2dSkJizlT4fonCiO3WiUXX6qMZOx74PPR0FqL2Iow2ST8gxlBqxJ9WTs+nYGSkOCjVaqfcQiEESjM+WtRP6BZeki68ri4irJjccPZHmt8LqV/x+wpLj6T9e9KISQ7xSRMfW0eHEbJWA0QPHI/toanw+/1q3kho67UkyZf0e46EVqKUi99RGHbT9xpUyfSfifakKq8PYM/F/QvaC5NwnWT9hFlTP6BZEK+U4yUlW23o6iw09iUSzrgkFcMtsulpAqXr7vv5OIOn4o+kned0skOHDlWi6S5duqjMTbm5ue5sUEOGDKE6deoosTfXsWjdurXX9iy4Zjzb77rrLnrqqaeoWbNmysl45JFHVEYn1yxDy5Yt6eKLL1ZZpzjblM1mo1GjRqnMUMVlfuJzDSWU6frrry9RKFhiOhUgKtlzNINsDhMlCdWimYMnUlQ8MgBhg/AnAMqFnQczg67fcTAjYucC4pwytPMDBw6kgwcPqmJ1PAPAIUqzZ892C6137typsjKFwgMPPKAcE647wTMSPXv2VPtkp8TF1KlTlSPRt29ftf9rrrlG1bYojilTpoR0LhMmTKCSgPAnEDUcz0uhHzcFjgv8amV9MlCMDJQGTpP+EgbPPPOMik3lJ1DBmDlzppoi58GjTZs29M0338RdpVUAmF93VqGtOYEdiy9WQE8BYsPO8839jh07qLCwUIUpdfUIQVq4cCG9++67AbfldVx/whMeKzjtLDspXPBu7ty5dNZZZ3n14UxP06ZNU+Lv48ePq7oYmZnBHXVp1oKL5pUFcCpAVPHq7LNp3U7/glBz19WiGUtCEwwBUJ51KrhA0ZtvvknnnHNO0H5Lliyh6667TjkBa9asUVPdvKxfvz6uKq0C4OLxme1pz5F0vwylUxY0pWV/VMeFAqUD6hGJsDPCD6Y4zOrpp5+mPXtKT8eEWBIQVeQVJtG9H5xL7RocVmkHbU4zLd5Ug7bklH6aN5DAlHH4E88Q8E0+pxDkGNlgvPLKKypO9v7771evn3zySeUYvPbaa8qJiJdKqwC42Hs0g25+oyf1aHFApdk9lW+h+etr0YHjyI4FShGEuYrwuMGhWx988AG99957NGbMGOVk8IMtHmOSkpLKf6aCB7VQU09FCy4xj+ei+8RSa1tdwnly6hSWYImbPRensOgibSsdI0TW7qhKkxc2pw8WNZMdChaOeS66+G5nOJUQ03fR353Tb9G+xlK/cK6d7ral+DQ+UbKCcBYOz4Wnu4MxcuRIGjBggFd11EBwlVTffjwL4aqiGi+VViM9nqjidx6LEm57LRb/hQW+PgsLpku8+JyDWjS3lc5FCbB1FlMpL+Ix9K5VoD6GyUI/ba5F7yxoQdOXNKUDJzK8+4Zhv1mU7buI9pHbfRbtbXXtvK7tD2ccLim6x9R8/9K1M2w2/6WwUGshm81nEYrrBQNZ/gJSvXp1JTb/5Zdf1BjRtGlTVUuDH0DdfffdfuG1EXcquGw5x5aVNa+//jo1bNhQxR7zoLp8+fIyPyYAIL6QHgYEWhhO+cc38q6FM3oEYvr06bR69eqgfTxhh6G4KqqutliutBqN4wkAIH4J1c4nIvv27VMz47xw7Q2e3eb0t61ataKXX3657MKf2KMJBk+llDUzZsxQ58EhAexQcEgAP9HjwiG+AyoAAJTWtPiuXbu80uulpKSI3bnfnXfeqQy0Z8YOEH3jCQAgzkH4kwinouUK25wR6vvvv1e6P04m8o9//MM9zn322Wd08803q1mLMnEqOO6XU2YFylsbiSwjL730ksrP68oDzM7F119/rdTvDz74YJkfHwCQmLDd08nZzUWJuOJphw4d3G0Oh0OJplkjwWFT/DTIEy54FKwya1lXWvXMCMIxteeffz6VNdEwngAAQCJSq1YtVeCOE4RwtI9rHPGEC/O5ammUiVPB8VbssXBBDAmuCsgVBstyOpwH7NGjR7vbOF6Y44ZdccW+8ADuGfvMsdAgsWhR5xi1rHOccguttPi3GnLhPJBwsFJCqyhSiPvl3OE8dewJPwThdLH//ve//RwKV5VUrqLqmXaWZzpcVVQ9K626jL+r0uqIESP8Kq267HAolVY9M4I0aNBAnTM7GVy8qSwozfEEdj6xMZFBnZocorpVc+nwyVRa+nsNspNUiRAkGmVl52Odl19+ma699tqgs+nsULCer8ycChYA8oAVaBDg/LqcpaSsOHTokHriJ8UV//bbb+I2HNP8+OOPl9k5geglK72IHr12LZ3T4Ji7bdQlm2jSnOY0a1W9cj03EJxs4ziNMH6kS2g9pZONVhTUp0lJPWmxpUnpXToPEXax/UKgQoUKfpVTOf1r1apV3e2elVYZDpfq1asXvfjii0rczZqMlStX0qRJk9R6V52Lsqq0GomMIGU5ngS080p47VFR22e1SqqgozIUqzhHALGitkmvKnQpI14ri3B+UuFpn8rbulWxdahf7RQ9PnAt1a2a5247mptMz37RllZvq+bdWRIhR8L3EBOJaH5mwm9Askhile2SEs59nFiN3P/9O4uKqEn2CfrHeduoc1MOdTTR8i016H9LmtG2g2dmL02+YaZOR1TY+Vjk119/VWMQP4xnQXZxcHry5s2bK21eKGhbIx7wghVwatu2rXoqFk3wrAY/fXMtHO8MEoP/XPOrl0PBpCU76fZLN1GHxofL7bxA8Q7FR8bbdB2tpEpUQMnkoB7ObTS58EP6m/3XuMhfzpVWWRznonv37qqYETsRbEc//vhjdYPv6ZxwpdXbb79d1Z3o3LmzCg+SKq3yjAjPlrDYjquxuhyT8s4IUpbjCex8YpJkcdDYwau8HAqmckYRPfb31ZSd5d0OooeWdY/Ryzctp/Nb5ahxOS3ZQb1a7qOXb1hMzWsdLb0DoU6Fm/bt29Phw/r3Pjz7zWNVqGi7IK643AULFqhYKwku9HTbbbdRWVCtWjUVOhAs9tgXFlMGElSC+IWfgLRvdCTgw79ruu6g1VurRvy8QPHwDEVNOunXbiGDRhd9R99aWpHNZI0pAR9XVg32muGpaF4C4aq0yksgXJVWyzojCBfiC1W8V5bjCex8YtKrVQ7VyJJTO6cmO2hAh100eUHziJ8XKJ7bLvxdORO+sHMxrM8memBa99K5jBBqu+GZX57hTk/3LjoZTHJQEkKeN3UVaeLpdc/QpMsvv7xMxdLJyckqxpbjil3wkyx+7Yo9BoBpWsv/ptSTprWgrYlWrqBfAq6rTrnU1bm9VI6DVIP+sE3/5JNP6LLLLlO6ipkzZ6rZhL1796pwqLlz59JHH30U1LGJlfEExD7NirHjzWoej9i5AH2qVcijs+t5RxF40rb+EaqcUVAqlxR2/gycgIMzpa5Zs0Zr4fvqtLTQi1GG/MiPnyxxTDA/weKnYizk4Jhbjr1icV1ZwtPyLB7keNwuXbqolLK5ubnubFAAMCfzk8JaD8qHTqbtSkMRjFQj+Hpt8AQrohlBonE8AbHNyYLi7HxyxM4F6HNJ2+JDalKsJdPY+AE7H3SGvCwI2ang+F829v/85z9V2kQehJ588kkV88tT9GXJwIEDlZDw0UcfVQWdeNDjuGJf8XaoiBodU9lvq7WdqZTnnKTPSBIElrKYMJKs+LMaHctNokoZ8g3o3HW1vSq2qqqvZU0kjhHNaLz9wZbglZ9tZKK1lrp/PX4Kff9eYLCJaEaQchlPXNWgPV+X1m83nGrHYdhMUZSt+76kbTV1K6oiuYagV7wqjuDC7dMb+r8HUdDtIXqe+2stuqHXloCmde762sUKpiUBukkSKkuftyj0L7kAWxZIl8O4Ucbjd++We4KuP3gihXKOpalwHcPnM/N9XSyw8xGnRGkjfv/9d5WdpG7dukoZzlMqeXmREUVxRhOutMppBFlIqJsuESQONoeZXvm6Fdkd/sZx0+4s+nx5g3I5LxCcJqbgBc/Wm2vTIXNmqVxGTIv7w4Ls8ijYV57jCYhd9h9Lp/cWNBXXzV9fi5b/WT3i5wSCYzU7qW6V3KB9Fv9ek5yllI0Jdj4GnIpnnnlGxVpdeOGFtH79ejVNzvFXXJEvUL0IACLNT79l0x2Tu6qnWXsOp9PveyvShO+a0/0fdKECWykIfUGpc5iCOwz/s3YqvYO5Ug3qLKDMwHgCwmHaT41p9LSO9PPv1WnPkXT6dUdlevbzNiqlbOJVH4h+7E4znSgm/PjbNfVL74Cw8yLvv/8+bd9eOvpEX6wlqYTK6Q4vueQS9ZrTHrJj8dBDD1Hv3r29is0BUJ78sa+iGmC88Ah7AtHFZ4521MUsG7oTlELfWM8uvYNhWjwqwHgCwmXllupq8SQiIa2gRHy/rj79vcsWcd3v+7K86lSEDey8COuQX3vtNZXhietXcIY/zux38803U8WK4V3/kO+wOK2gy6FwwUWRnn/+efr+++/DOhkAQOIyy3kOzaZWfu1FZKF/p1xBBabSE9ibnPoLKDswngCQWExdfBb9vreCX/vxvCR66etzSvVYsPMyrFdhKcELL7xAx44dowMHDqh/N2nSREUeRXSmgutFBIKrwsYErkA792tT6YqydUSkJr3967ZJ70Gqsim+U+n9a7aZhCf/hq6YUJo18Pxc3N0kgV3ZzjiIT7qEtrCEk2Fcd/026bgl/J6FsS8J334OMtNd5r/TRbSJrnT+QpUonzaaatL7aZ1pi7mU46N9TECwfqDsKNPxhH8Pnr/ZkiaUiXZRtq44WNpWU4AtIomchXPxq4zujF67DwKgez8gfJ8Mi/dnludIofum9aR+rXfTeS32qSKGv+ysRrN+aURHctPI89mR7/5MphBLoMPOB+SWW26hN954Q81SMJzWe/jw4XTnnXfSokWLqKQguBwAEDUYJjN9R2fTd5YzoU52cxnc2WNaHAAAyoVCu4W+XttALUEd3HCBnQ9aosHlULgijjjrHpdsCAe4/QCAxMMIYQEAABB7wM4HTA2+e/duv/Zdu3ZFXlMBAACxDlINAgBAfAM7L8PZW7nI6Mcff0x79uyhnTt30v/+9z8VEjV48GAKB4Q/AQAAAAAAkAC88cYbdMcdd6iC0i5SUlKUePu///1vWPtOTKeC9UWeGiNtMbTQj4rf1pAKg0rhg2EIckVRtnhcoZ9ds8q2JAiUqqOK56Ih4FNPFvQqq4pVWXUQLoooyhZF5Jqfj+610xWv64o9db8rwndAR4RtCOeh/TuR+knfT939+egsfF8XC2JtQWmLssMgLFG2dkVtzX66plW3Qrfv+Yk67ZLbfaOcPjNtdIXv5YH0nTDCGA99RNmqW3KycFxhf5KmwmotUQV4N7DzIlWrVqWpU6fShAkTaOvWrUpPwZmfSqP4KcKfAAAJB6bFAQAgvilrO//6669Tw4YN1c14165dVc22QHz66adKBM16hoyMDGrXrh198MEHfg9aH330UapVqxalpaVRv3796I8//vDqc+TIERWixNoH3heHMZ06dapE58/74PM4++yzS8WhYOBUAAASE4i0AQAgvikjOz9jxgyVQWnMmDG0evVqatu2LfXv31/VfJCoUqUK/ec//6GlS5eqgnNcgI6X7777zt3nueeeo1dffZUmTpxIy5YtU84H77OgoMDdhx2KDRs20Jw5c2jWrFkq/eutt95K0QKcCgBA4oGsIAAAEN+UoZ1/6aWXVF0Hdgy4GjU7Aunp6TR58mSxf+/evemqq66ili1bqlAjrgdxzjnn0E8//XT6VA2Dxo0bRw8//DBdccUVat37779Pe/fupc8//1z12bRpE82ePZvefvttNTPSs2dPGj9+PE2fPl31iwYSU1Nh9nan9OO4A+yrmH7aRe3Mum3haCCkWEapnxDfKOkYhP2JBXAk/YT/3sTftkmam9StsKaDFC8qxYZKMZ9Cm9hP97pb9IoJim2CVkJs09RZ+H7P5O+stB3ptZXm7y7ExyO6U94lnRYH5Q8X5vQszmnoxLobejH72josXcLRT2hqJaRCpRKGrl7NWTK9iOEQqhCK51bSaoVRhq7mpYwJqxiu9B2Txi+nMB4mCb8zcSwV9pfkq6lwlKmdP3HihFc7i5d58aWoqIhWrVpFo0ePdreZzWYVrsQzEcXB90Pz58+nzZs307PPPqvatm3bRvv371f7cJGVlaWcB97noEGD1F8OefKsJcH9+dg8s8FOS3mDmQoAQOKBmQoAAIhvQrTz9erVUzfyrmXs2LHibg8dOkQOh4Oys7O92rOzs5VjEIjjx49TZmYmJScn04ABA9QsA6d3ZVzbBdsn/61Ro4bXeqvVqkKrgh03kiTmTAUAIKHBTAUAAMQ3odp53+Jv0ixFOFSoUIHWrl2rhNXz5s1TmozGjRur0Kh4AU4FACDxQKpBAACIb0K08+xQ6FSUrlatGlksFsrJyfFqz8nJoZo1awbcjsOUmjZtqv7NWZdYI8GzIexUuLbjfXD2J899cl+G+/gKwe12u8oIFey4kQThTwCAxAPhTwAAEN+UkZ3n8KWOHTuq2QYXTqdTve7WrZv2fnibwsJC9e9GjRopx8Bzn6zxYK2Ea5/899ixY0rP4YK1Gbwf1l5EA4npVHChLM/F4r9wcTq/xVzSxVTyxVLKi9mstSjRlNZiEZaSb2sSFv3jaixcTMdnMSUJi3gemufL109rsQiL0I9F1BqL/FlK3yuKjsWit7gTK3gtPr/hEIvflVX+ci4mxFk7XE+8eBD49ttvg24zc+ZMatGihcoT3qZNG/rmm2/KNXd5wsACbJ/FEBYl3vZZWGzsu0j9woIFs76L2X9xCdKLW6Rt5cMK2+oi2S+d9wW0rwsLrn0X2UYWv8j7khbN8UsaJ5OS/JeUZP8lWVikbaOkTgWHLr311lv03nvvqRmHESNGUG5ursoGxQwZMsRLyM0zEpwGlovNcf8XX3xR1am4/vrr//pZmOiuu+6ip556ir788ktat26d2kft2rXpyiuvVH04c9TFF1+ssk5xTYzFixerKtgs4uZ+0QDCnwAAiQff7+nc84V4X1i3bl165plnqFmzZsoZ4AGH0wOuWbNGFRjyZcmSJXTdddepAeeyyy6jadOmqQGE8563bt3aK3c574ufZj3yyCMqd/nGjRvdBYvYodi3b58atGw2mxrYOHc57w8AABKSMrLzzMCBA+ngwYPqgQ+LpDlEafbs2W6h9c6dO1W4kwt2OP71r3/R7t271cMhfpD04Ycfqv24eOCBB1Q/tt08I8EpY3mfnoXpuBI2OxJ9+/ZV+7/mmmvU+BAtmAwp12ecwlNJrOiv98oTZE478yGZ8/2fpljyhLZ8/31ahTZLQfF9rAWGVpul0P/bbikQ2gr9U62ZC+1+bSahn6lI6Fdk8z9pm9BmF1K82f33Zwht5BB+yUK6QcMp9Cvp11ZKq6ebulE3faxVahP8d+mpi28KPX6rKUJbsn+bU2pL8T8XR4r/+3WkmovtZ0/1vyZSm0MozGkX2hxpQpvYz/+zdqZ5f0+c+QW0665HVXaNYDGxLhvQ4o6nyZJSfAVRR2EB/fbqQ6KAT1fEx5k5nn/+eTV74AsPKDyIcBEjF+eee64aoDjvOZtnfgJ177330n333afW83vkgevdd99VT6j4qRfnSV+xYoU71SAPRJdeeqkawKLlCVakcH3G/arcRFZzsrvdKCry6mcINk5Md6o546Cbclq0QbrbCrML2jMJ0syEkC5XwpCugbStZJd9rqloz4WxQPez0E35K9tqf5tpEuyySbLfunZe6hfG5y2lEy9xzInwUZg0Px/dsZ+k/UlI74tnMDx37yykuVtfLTM7X9x+QfEkZvgTACCxKaNUg55wykEuSsROQ6A4W8477pmXnOFZCFeu8+Jyl7v2ESx3OQAAJCTQzkUchD8BABKOskw1yLGw7EQUFBSonOSfffaZmkmQYIehuLzkrrZYzl0OAACRBqnDI09COhWG2VCL+3UpVs+W+imxKRXf5hQ+DbMQcaRdOVmsCi1MRQrbilO2uiFHmtWzySRMZUuhAdJxfadUpT66AkDNatfalbK1p8qFyuNSm1Sh3CpU1Laa9No0vz++30fd77HUpvvbkduM4qvWm6Mj1SDTvHlzlYucp9I//vhjGjp0KP3www8BHQtQRgQRIwdCO/xJCjkSvFST1JYk/UBKGd33rVuN22nWq7ztCMMOJxK616Sk107aThojJbsp3RCJ4cDSxsLYp5uwQBg3jWTvUDRDCsMKBlKHRxyEPwEAEo8ynBbndIOci5xTDnKYVNu2bemVV14R+3IKwWC5zj1zlwfrE+25ywEAIOIg/CniwKkIA6shPNUCAEQ9phCWcPHMRe4Lh0l55iVnOIOTS4MRL7nLYxmL9EQeABD1RNLOgwQOfwoHi+Ggm2gpXUcrqA4dp4MpmfSxowNNsp9HRZRElSmX+iT/TlZy0jJ7I9rjrFLepwwAiNC0OOclv+SSS6h+/fp08uRJldJ14cKF9N1336n1nHe8Tp06bqH3nXfeSb169VI5ywcMGKCE3StXrqRJkyb55S7nNLWulLKBcpdzxihOKRttuctjjYwUG11//p90Udu9VDHNRjsPpdNnyxrQrFV11S1Ircp51K7hYbI7zLT8z+p0PM87Sw0AIApA+FPEgVMRIi/SJ3QxbXS/rm46RSOsi6idaRetNerRMMtiSjadnsHgTHdf286hJwv+RnZcagDiXsDHYUjsOHDNCM7SxIXw2KG48MILxdzl3bt3V47Hww8/TA899JByHD7//HN3jYp4yV0eS6Qm2en5G1ZQs1on3G31q+XRnQM2UcPqpygj1U4XtNnnliMU2c00fXEj+nBxi/I7aQCAHxBqR56EdCpMFkMtLgyPfwdr62DaSRcbZxwKT7pZtlE32ubVxoPO5cm/0qmKqfRc7qVe65yS5lcUy/q3mYWoK6dDEBYLojlDOrAk4JLE1tK2ukhCr7+cL68mzVzqYt70Ep+anlCbtIXaYeQvFwTYUpskwpfanJIoW/hOif182uQ+mmJr7X56v0WT1Rn0dXk9wXrnnXeCrudZC1+uvfZatQSCZyueeOIJtQSCMz2h0J0PvhWdpd+qQP+2e7wcCk/+1nmXnylLtjppSK8tdOhUOs3+pT6VCLH2gGZNCslmhiOODqd0VRmLsqWaFHLtCinRhrTDGCzTZS7hddf8bCRbbaJwvnea9w3SeOib1ICrdocCZioiDjQVIXCxsaFEF/nKtDWUaRIq4AEAyo8yEGmD2Oe8lvtLdF/2965by+aEAAAlB3Y+osCpCIEUEqpFapBmslETy8ESbQsAKLtpcZ0FJBY881AS6lfLpbTkko0RAIDSB3Y+8sCpCIFl1KjEF/qkUXypeABAhECqQRCAX3aULLlGoc2s9BUAgCgBdj7iJKSmoqR8Z2pFfxo/UFM65Lcuz0iidJNQqY6IfrPVpK0O74q3oHRJsjjo2va/Uf9WW6lqRj7tPlaBZq1rSrPWN0XCOOAHBHwgEF+urE+Xdtitsj75kl9kobRkOZX4ok21yCHE8oPSo1J6If2j51bq03qvEsxv3pNFHy1pSEt/x/gK/IGdjzyJ6VRwhV7PKr2aVYGLkiw01BhCz9i+oPOMLe72lUZ9eon60ovGJ1TLdMLP2Xg6/1K/6sQmSSxrN/TE25JwNYwq26IAWfObYSoKR6itWfVTqHJr8vjQzCYnPXnpQmpX90yBsIZVT9Co3qupSY0T9OqirsGPKYkkdfuFUz07SbNNrJ4tiLKThH5CmyT+N4S34avLl/roVoaX2sT9Sd87sUK3z/dEqLodFAj44h+2a57CTl/bJ9hCk8VCh3Iz6cFpXemeAb9S05qn7bnDaaLFv9Wgb9bUpTHXrvVzLA4cT6V3FzX3P4cQK3oXS2mLsiVE26dZZZvKrnZTxbQiGjdkGdWpmudua13/GLWuv5Ze+7YFfbGifsgib1G8XV4In61RmhW1w0Cy8+SUEgloisGlftKYluw9IDgdIYYXws5HnMR0KsLggKki3Zx8A9U3DlN94yjlFGXRVqqu1l1Lw2mI8TNdZGyiJHLQUmcTet/WjXY4Tq8HZUPPxju9HApPLmn1J3214SzadrgyLj9wgydYIBh/5mTRvyafR41rHKfKafm041AmHTyRptaNeqcbXdN1O3VofEjVqfjpt2z6dFlDOlZwej0oG67ustXLofDk5gv+oO9/qU35RbilAWeAnY88+AWWkJ2mqmrxdOAPUQV6iS6kVwtO56R3EU0PQ+KR8xrvCLr+/CY74FQAb/AEC2iw9UAWGY5Mr7adhzLp5a/P1BGJyqfecUivlvsCrktPcVDXZgdp4YZaET0nEOXAzkccOBUg5kmxBp9yT7UiIwvwxuQ8vRSHTh8AQNmTmhTczqck4ccKvIGdjzwJ6VSYLU4ye6QNdAgpBGU9gl7xL9/4cbM9jFj0JCFu0SG0OfViXqXc++ZSLgBksuvqFqTzE95IMUWrNhyoSZ3r7w24ft2B2kRJSX8d01TyOGVJeyK0GZpF7XQ1FZJWwinFn0o6nVJsE7+fkh5JW3uhV+jOEH6fFt82S3QUvwNRBP+WzGe+jCbHXzbAhVRITdeeiR01tVmabWKhO13Kq/hdGbJuVxXq3UqereCPcv3OSkG3FwurhnOddLV44XyOEjrfnzIuQqjQrKOrq08UtYM+Y58R6rWEnY84SFUBYp7Zv59Fx/NTxHU7jlain3fWi/g5gejGZBjaCwCg/Jn5c2OyCQ/UmB83ZtOeIxkRPycQ3cDOR56EnKkA8cXxgjQaPbs/3Xf+j9S46lF3+9q9NemFH86j7MxT1L/5H1SzwknKOVWBvvu9Ge09kVWu5wzKGTzBAiCm+GN/JXp8Znu645KNVCOrwJ2Va8H6mjRuVis6q9ZxurDtXspKt9HWAxVo9pq6dCxPftgEEgTY+YgDpwLEBduOVKGRn19BzaodomoZubT7WBbtOl6J+jX7g+7quYQsHilHr2m9nsYv6U7f/X5WuZ4zKD+QFQSA2GPZHzXohj+rU+v6RygjxU5/7MuiQyeS6ZZ+v9PA7tvd/frQfhrUYys9OqMD/bqjarmeMyg/YOcjD8KfQFzxx6FqtHRHA+VQZGee9HMoGH59e/clVKuCd00RkECg0ioAMYnTMClHYenv2XToZCp1aXbQy6FwkZHioEeuWasKo4IEBXY+4iTkTAWLtD2F2qJgVGhzJgnF6QQRn69QVRK4mpKENiFe1OkQCvYkC6I+IfTbZOgJtcPyNqWOZkE0a/dvM0mF+BzCtsJnoSMmvKjVVj+HwgW3X9zyT5qytqt+oTvhfCXxvizeNpdclC31S5b2pynAFvr5alhP9/PdV/F9wi9+J3zfpd+dr1BbEHMHA0+wEoCkZCJLckCboa2XkUSlTr3vGxfT09mfdpG8SAhwY4wBHXYHXFcpw0bntTpACzbUDu8gpV3EMJo/x3AK1WpuW9Lx0BmiUBt2PvIkpFMBEoOamSeDrueZDJCgINYWgLigZqX8oOuzs4KvB3EM7HzEQfgTiFv2n6wYdP2+U8HXg/jF9QRLZwEARC/7jgavZL7/GCqdJyqw85EHTgWIW77b0pzsTnmambOGfP9ni4ifE4gSEGsLQFwwa1XglOFHTyXT4s3ZET0fkDh2/vXXX6eGDRtSamoqde3alZYvXx6w71tvvUXnnXceVa5cWS39+vXz628YBj366KNUq1YtSktLU33++OMPrz5HjhyhwYMHU8WKFalSpUo0bNgwOnXqFEULcCpA3HIgtwK9tKS3n2PBDsW4n3thpiLBwSwFALHPyi3VaNqPjfzaTxVY6YlP2pPNIehaQMJQVnZ+xowZdM8999CYMWNo9erV1LZtW+rfvz8dOHBA7L9w4UK67rrraMGCBbR06VKqV68eXXTRRbRnzx53n+eee45effVVmjhxIi1btowyMjLUPgsKTqdQZtih2LBhA82ZM4dmzZpFixYtoltvvZWihYTUVHA1Xs+KvA6hGq8kmDXshpYQ1leEbRIqaottguiVb4C1qmdLCS6kH4v05F5T/GSWRFhSMSJJqC21OXXb/Jt0RZYL9raiDd/UpYsabaKaGScoJ68ifbe1JR3Iq0j0l4bTEKtna1YBFaus6wq1NStlC6JspyDWd0gCbA+d6pl+pCXeLlWhtiC2lkTZRpL/h20Wfp9WH2G2I0ShthIZ6nyHUPwuZjFSk8iwJIVkM0yiLRBuSh2OkgtXzZrVsyHe9r9O4jWx0LuLWtCi32rThefsoYppRbTtYEX67td6dDI/mci1iXCNTdpVsTXbpPML53Msxce+4jinifjbkQTYmu/BEMfS4tsMKflMOdn5l156iYYPH0433XSTes2OwNdff02TJ0+mBx980K//1KlTvV6//fbb9Mknn9C8efNoyJAhapZi3Lhx9PDDD9MVV1yh+rz//vuUnZ1Nn3/+OQ0aNIg2bdpEs2fPphUrVlCnTp1Un/Hjx9Oll15KL7zwAtWuHWZCglIgIZ0KkFiwA/Hhhq7lfRogikBWEADii60HKtKbc//SyYWYJQjEJ6Ha+RMnvNPMp6SkqMWXoqIiWrVqFY0ePdrdZjabVbgSz0LokJeXRzabjapUqaJeb9u2jfbv36/24SIrK0uFVfE+2angvxzy5HIoGO7Px+aZjauuuorKm5j55f33v/+l7t27U3p6urqoAAAQbbG2Y8eOpc6dO1OFChWoRo0adOWVV9LmzZuL3W7mzJnUokULFZvbpk0b+uabb+Iu1hYAAKLZznNIEt/Iuxa25xKHDh0ih8OhZhE8yc7OVo6BDv/+97/VzILLiXBtF2yf/JfHFU+sVqtyTHSPW9bEjFPBnuG1115LI0aMKO9TAQDEOBxCqLuEwg8//EAjR46kn3/+WcW88pMojpvNzc0NuM2SJUtUrC07AWvWrFGOCC/r16+Pq1hbAACIZju/a9cuOn78uHvxnIkoTZ555hmaPn06ffbZZ+pBUjwRM+FPjz/+uPr77rvvlvepAABinTLKX87xrp6wveInSzxVfv7554vbvPLKK3TxxRfT/fffr14/+eSTyjF47bXXlBMRL7G2AAAQzXaeZ3l5KY5q1aqRxWKhnJwcr/acnByqWbNm0G3ZHrNTMXfuXDrnnHPc7a7teB88I+25z3bt2rn7+ArB7Xa7mqUu7riRImacipJQWFioFheueLkkJdQ+I7SzCZUcDaGys2GTxKxCBWAf8bJYPVsUH+v1E4psc0/SQxIE6t05GYLgTKooLonpTJLw2SEpuAy9N1xCAa2ukEwUoUniMkFYLQnzROG/IPJ3SP3ECtgmLfG2LMAuXpSt2nwrw2tU3Q7UZohtetWzLUn+otjkJO9MBw6hTzBMTkMtOv1CibX1hZ92Ma64WQmOk+UsIp7wLAQ7DPEUaxtpO+9MsZLTQ6ht9q2orSu+tfln1TBJQm3NKtva6J5fjCGJo9lx9utnCDbYKYmtNZOPCEJgSYQvVkEPR7wtUdqfYxl/L7RF3tJYKonhxQQnUuVt7zZD+z6nZHZel+TkZOrYsaMSWfOsMuN0OtXrUaNGBdyOZ5w5lP+7777zstVMo0aNlGPA+3A5EWzL2H67InS6detGx44dUw+p+PjM/Pnz1bF5PIgGYib8qSRwPJxnfBzHywEAQKhFkXRjbT1hQ3/XXXdRjx49qHXr1gH7scNQXBytqy2WY23LCth5AECki9/xgyCuPfHee++pmWK+8c/NzXVng+KMTp7hU88++yw98sgjKjsU17Zgu8yLS/fGDzl4vHjqqafoyy+/pHXr1ql98Cyzy3Fp2bKlmtXmrFNc42Lx4sXKieEHS9EyG12uTgWn3eILGWz57bffSrx//kA94+M4Xg4AAEIV8JUk1pa1FayL4NhZUHbAzgMAIl38buDAgSqUiRNo8MzC2rVrVQiq6+HPzp07ad++fe7+EyZMUNrgv//97yq8ybXwPlw88MADdPvttystHCf8YIeD9+mpu+DUtJzUo2/fviq8tWfPnjRp0qSo+QKUa/jTvffeSzfeeGPQPo0bNy7x/nVDFAAAiUWoqQZ1Y21d8NMjl1i6bt26QfvylHew2Nx4ibUtK2DnAQDlkTqc7XygcKeFCxd6vd6+fXvx52Ey0RNPPKGWQPDs87Rp0yhaKVenonr16moBAICIUkZFkTg2nJ80cVYPHlQ4TrY4OE6W42h56tsFC7W5PZ5ibQEAIKKgyGnEiRmhNk8l8VM3/sv5gXmqiWnatCllZmaGtK/UJBtZPASxNru/MMtp16tibAgVpZ2O4MLtgEJtXfG2IGCTb350RWPC7gShm1kQdEviKrPQZhJE2SZJgC0IpqRqnpKwTwvdKqDSNZGEZIJIUKoMKomjpf3JImqzVqVsaVtHchhVtpODvz59TGk7ofK8UCmbkoXq2YLgOkloS/UValuFEvXl8ASLQ574KdIXX3yhalW49Aysw+D6EgzHydapU8ety7jzzjupV69e9OKLL9KAAQNUuNTKlSvdU9qesbbNmjVTTgbH5gaKteWMUZzKNtpibSONM9VKTmvgIc4s/O5F0a8k3LUL3ze7ZpXt0hZ0h4N0fuI5Sza4jN+HVAFb+CjEitXSZ6srwBarYmtW3paIhLje9zOT3n9Jx8xQhNqa46s4bkoJTnzGSKfTFFUzFSCGnQqOW2NBjIv27durvwsWLKDevXuX45kBAGKOMkopy3GzjK9NmjJlijvUkx+McFYmF1zUkx0RThn70EMPKceBMz95irs51pZFgBxryzMSHEcrxdqyI8Gxtrz/a665RtW2AACAhKSM7DyIA6eC872jRgUAoDQoqydYOjNovrG2DBf25CWeY20BACCSYKYi8sSMUwEAAKUGh3Po5CYPMX85AACAKAF2PuLAqQAAJB6YFgcAgPgGdj7iJKRTkWy1k9WjinahILpzCMJim9Bm2AUBm49+TRIXyQJs4WR12yRlsbBDSTRlsQmH0BQqm6X3Lwq1pcrjeqJsnafFumEq0vuSK6OSnrC6hJVB1ba6bWJFbV2httBPs813W11RtlQp2xD6SaJsa7J/W0qy/+8zPanI67Xd53Vx8JXSCn8Kaa8gmrCnWYisZ+y838/SrCfelkS6UlVoMktVtoU2h7PkgmkJXeF3aYuyJbtcigk0pKrL4qa6Imrd6tniZyt+WbSOoSsG1xZD6yB9DmHsX0yWEk6VbU3xtt93IFShNux8xElIpwIAkOAg1SAAAMQ3sPMRB04FACDhgIAPAADiG9j5yAOnAgCQeCDWFgAA4hvY+YiTkE5FRlIRWT3iz4vs/pfBLugnHFJBPCnU1GkpvqCbFBuorZ/QLWAnxW1qbivFfNqEomZSSKo1DE2FFB4sxHOWtFiNrqZC7CdqKkiviI9wTQzh1yf109VPSJoHnaJ2ql+KdAzfPoaepkIoamcStBIWST/hU9SOSUuyib9hT2xCn+JihHWKQYVTMAqUL45UC5k8tHN+v3Mxrlso+lkofLck3ZhNCB4Xxgwy2Uuus4h2/YR0fjq/M0mzIHbULEwnFCDV1k9o6yI0+5VH8buS9gmEVExP9xDSV0L6uM3F9xO3CwLsfORJSKcCAJDg8ECnc38WRcWPAQAAhADsfMSBUwEASDjwBAsAAOIb2PnIA6cCAJB4oCgSAADEN7DzEQdOBQAg4UBWEAAAiG9g5yNPQjoVGUk2SvIUajv8BVd2pyDUFtqKBMG1b7E7sXabqDjSFHTpiq1FAbbUJhS1swjFyqTidw7h/QsCbLNdEHqJAnYqmdCvlIvfaV87Sbxt1S2cR5rF70ivzVqyonaB+vmKsJ0pekXtKMX/Q7QI4u1koahdWrK/KDYzudCvrWJygddrmy204nfIXx7/2NJNZHjYecNkLvY3btW0rWbNgmsmXeGuSbNwXjhiW0mU7XCUqQBbddMQl2sXsIuEKFvzGGLxO81Cd2GJt0szeYSuZsysWUzPJFxPoUlCJ7GMOH4H3alR9iJ24EVCOhUAgMSGnVfRgRX6AQAAiD1g5yMPnAoAQOKBJ1gAABDfwM5HHDgVAIDEA0WRAAAgvoGdjzhwKgAACQdSDQIAQHwDOx95EtKpqGDNp+QkR1Chtk1Q0ToEUbYhtPkWnpaE2k5txZFJq8kIp02qCm3TEyCb7YaeeFsQFktVxSXxtnTSJY1119XHy0JtqQKv0E8SYFt0K2prirItmmJr3YraguDaV5gtirJT/YWeZqFSdnKKvwA7TWjLEEXZ/m1ZSfler4usEGoDb+xpZjKSzYGzOWiKsi3Sb1dKWiEJtQXRr0lX0G3TFMxKIlO7Xa9StrStIN4WxdYlFbfqitejSZStu21pV9R2lvxmusSflzQGi2Of9N3WLZ8toDHmSmNwUBD+FHES0qkAACQ4huaAjaQgAAAQm8DORxw4FQCAhAPT4gAAEN/AzkceOBUAgAQV8OnkL4/EyQAAACh1YOcjjmawGwAAxBGuWFudJUQWLVpEl19+OdWuXVvFyn/++efFbrNw4ULq0KEDpaSkUNOmTendd9/16/P6669Tw4YNKTU1lbp27UrLly/3Wl9QUEAjR46kqlWrUmZmJl1zzTWUk5MT8vkDAEBcUIZ2Xscme7JhwwZlk7k/jwvjxo0jiVi38wk5U5GVVEApnkJtQR1rN/QqajuFNsNH5WsTVL/SV9jpU/FVIbSJgmHtCtBCm02vn8lf+0eGVAFaKNIq6bfkitrSGxG2LeETZLmidtkLtUXxtrXk/XQF2HKVbUOrzfCtjC1UypZF2XYtUXYFQYBdKcW7UjZTOTnPv5+PULswSVK1BoHfiqlshJK5ubnUtm1buvnmm+nqq68utv+2bdtowIAB9M9//pOmTp1K8+bNo1tuuYVq1apF/fv3V31mzJhB99xzD02cOFENNDwg8brNmzdTjRo1VJ+7776bvv76a5o5cyZlZWXRqFGj1PEXL15MiYg9jb/DgQWzktBU/I0LomyLpiDXLGwrYtYV2wpfSElELZ2fU1OULVXZFsXbgv0Wq0f7vDlLKYuyrdYwqmJLFaA1Bdia/aRK0eGgJcIWE56E0SYlVRGSBoiabGF/2olWfA8b6qUsQzuvY5M9ycvLo8aNG9O1116rbLVEPNh5zFQAABI21lZnCZVLLrmEnnrqKbrqqqu0+vMA0qhRI3rxxRepZcuWapD4+9//Ti+//LK7z0svvUTDhw+nm266iVq1aqW2SU9Pp8mTJ6v1x48fp3feeUf1u+CCC6hjx440ZcoUWrJkCf38888hvwcAAIh1ytLOF2eTfencuTM9//zzNGjQIDUjLREPdh5OBQAg8QhxWvzEiRNeS2Gh/yxLSVm6dCn169fPq42fTnE7U1RURKtWrfLqYzab1WtXH15vs9m8+rRo0YLq16/v7gMAAAlFGdl5HZscKvFi5+FUAAASjxAHm3r16qmpZtcyduzYUjuV/fv3U3Z2tlcbv+ZBLT8/nw4dOkQOh0Psw9u69pGcnEyVKlUK2AcAABKKMrLzOjY5VOLFziekpgIAkOA4VFoQzX5Eu3btoooVK7qbA01fAwAAiBJg5yNOQjoVVZNOUWrSGaWrXShPLFW8ltp8Rdm62E3CMYV+km5Zrm4pid8k0SFpVXaWxNsmSfQriLerGrlUZFgol1I9OupW1Bb6GaWY6lNTlC1X99QUVoch3jZ0Rdna4m1BlJ0kXLxk4cL7CLMtgig7Kdn/C5Ce6l/dOjNFqIotiLIrCaLsKkm5fm3Vkk56vS5IEr6IpZi/nB0KT6eiNKlZs6Zf9g5+zcdLS0sji8WiFqkPb+vaB0+fHzt2zOsplmefRMOe7ivULl5AKyZoEBNolNy2CD97IqkgvGgLNYW1kthatxqxhnjbbDKoYmoh5RdaqdBuCfrufIc6qcq4JHDWrpStK8qWBN1hVPLWFmDrxoOE9Xk7ixfvS98JUZStV1FbErlLQn3DqjdYy7+94vuUh52vVq1asTY5VHT2GQt2HuFPoNS42LqOPs14gxZmPU8/ZT1Db2R8QC0s+3CFQcKlGgyFbt26qYxPnsyZM0e1MzzdzYI8zz5Op1O9dvXh9UlJSV59OGPIzp073X0ACB+Drj13K30waiHNvGcBfXr/XHrwyl+oWgX/BwQAxKud17HJoRIvdj4hZypA6fP3pJU0Jm2W+zU/sOiRtIXaWXfR0JM30x/O6PCiAXA/jdPJSyw9tSuGU6dO0Z9//umVMnbt2rVUpUoVJagbPXo07dmzh95//321nlPJvvbaa/TAAw+oNLTz58+njz76SKUNdMFpBocOHUqdOnWiLl26qFSDnLqWs4QwHP87bNgw1Y+Pw0/bbr/9djXQnHvuufjQQakwqv9G+lunne7XyVaD+rbZR63qHqNR73Sjk0VpuNIgIex8cTZ5yJAhVKdOHbcug2cYNm7c6P43jwE8LnCtCa5NFC92Hk4FCJskstOolPniugxTEd2W+gPdd2ogrjSIHnSfTpVgpmLlypXUp08f92seABgeLLio3b59+9STJRecTpYdCM4//sorr1DdunXp7bffdteoYAYOHEgHDx6kRx99VAny2rVrR7Nnz/YS9XEKWs4WwsWQOGsJb//GG2+EfP4ASNSslEcDOpz53npSq3K+cjamLmmOiwcSws4XZ5N37typ7LGLvXv3Uvv27d2vX3jhBbX06tVLFT/V2Wcs2HmTYURgfj9K4Gwq7Ok9/PNFlJp5JgB9f2GWX99DRRl+bUcK0/3ajhf4P5nJLfSuQlZQ6B/sbi8UNBVCm6nIP0LNbBPiT4uEAk1Cm1TAziy1SZoKqaidnaizZRtNzniPAmEzzNTp6CN+QcdyQTyhDZqKuNRUVE7xLmDHVEnx109UTz7l11Yj+YTX64JTdnqky3yVxztYTKzLBvRrfAdZzcWLre3OQpq79dVi9wuiB9dn3Pzup8mSckbXZfX5uln8v35kLfD/bVjzhbZC/9+LpUAoDlno/5uxCG2mIv/fkalAaCsSDLNNaCv0/w0aUj9hf0aRsK3DQVd23k7/umgTBeKPfRVp5JTzitVGmJKsWnoHsZ9ZUysBTUW5aCooxX8Qcqb6V2V1Zvi32TL9P0dbpvcx7LYCWv7lI7DzUUxCzlRUsuZRmochsgmKWb4R9sUptpVMqF0o3CnbNMXBTsGwSpoxSQgsOSSSKFu6aZWcD3Y0TMWUxzSTQc5UgwwNp0IUq1EZC7V1++kKsHX7SY6BRc9ZkMRvYlVsoZ8hOBAmoc3XiUjWdCCkStmyKNv/rq6qINSuluTvVFS3egu18632qHmCBaIDWzrf0AT+XYrVf8UkGP7dDFG4699PLKgtGGtLGFV+TTrCXWVv/dsMi2RwpQvjIEsxYSRmocLy6RU+71cQYGuLsiUHwipVxdbcn/Q5imJw/yZx0A0H6foJfoBWtWzJgbBLbYLdlL5P0nvV/Cy0xeZS4gSf746YkCYYsPMRJyGdClC6/GLUpZNGClUwyYViljobk4GcACCaUANz2cTaAhCPrNpWLfj6rcHXAxBxYOcjDrI/gbDJoxSa7OwuruMZnzft5+Mqg+jCcOovAADafrACLdokJ9w4npdEX6xogKsEogvY+YiDmQpQKrzp7EU2stJN5sVU1XQ6hOUPZ3V6wX4RrTYayCFMAJQXmBYHIGSe/eIcOnIqhfq33U1pf4VH/rqjMr323dl08GRagFhVAMoJ2PmIk5BORTXLSUr3iP2TdBEOMeCWSqSpMAmxqFKbZJDtQpylU4gXdQqxoSZJP2HVazPZhX4BNBUu3qIe9K7Rlc5yHqBCstIWS40zwcK6omzpwbB0qUoYlaJb6C4SmgpJPyFqIARhtbStKLZOEsSjQltSiv+Hm5TkHYObnqwnwK4oaCok/UT1FEGAneQtwGayk477tdW0HvN6nWuRgo+DgGnxuMeRaZCRagSO2dYsVifGcYs2Q6+om24svlnoJ4b26+p+hH4mKbxPsiN/XQO2EhMWtqP3lpxNdSqfohP5KZRz4nQCE1OyXhE7k64uIpx+gi7CkLYVdTWan2M46OoMdLf11dBI+glBqG9Iwn9JjyHpYKRzk46rq5/Q0IWGPHEMOx9xEtKpAGWHzWSljabauMQgusETLABKTF5REv2RUxlXEEQ3sPMRB04FACDxUDptnexPkTgZAAAApQ7sfMSBUwEASDzwBAsAAOIb2PmIA6cCAJB4qJz9GgG6Qm5/AAAAMQDsfMRJSKci23KCMjyEzVJROweVvNCd2UdF7Ps6YJsgyi40C1UmhYJFDqG6pWEVxGo24b0KFZpMDv82p0OvQrfJKe1P6le2ouzSFm+LAmxRlC2IrXVF2cK2JPQjqSq2tfgCdpIAm0lJ9hfspfu0ZSbribIrCwXsqiTlahW1k0TZ1a3+4u2aFu/id6ekIl7BwBOsuMeR4SAjzRFQgCsVORPzc4iCbs0ieaId0SucJ+TPkBF1xYINlrYVhdVC0g+hmJ6hqZr1K2wnVsAOR6gtjH1S1UFzORW6E6tW630+hvBQQ/ps/RTMmgURperpYkE83YKFmoi/M+Ez8/0cDeG6BT8QipxGmoR0KgAACQ4GGwAAiG9g5yMOnAoAgDY180/SZXt/oxYnDlKuNZmW1qlHP2fXJ6eUTjOKMRwOMozi09AazhBT1QIAQIzTNPs4XdZ+O9WpkkuHT6bSt+sb0i87q1OsATsfeeBUAAC0aHt0Hz25bg6lOc9Mj3c/vJNWVttOz7Y7n5xSKEE0P8GScvRL/QAAIEG4tN0Our3/OldpEkWfs/fSR8ub0js/nE0xBex8xImhuwAAQHlhMZz04KYfvBwKF50O7aGLdv9BMTktrrMAAEACUC0zn0ZdtN7LoXDxf13+pNZ1DlNMATsfcRJypqK6JY8yPYXaYfhWOiJsq9k/hMIqqJSThH65Zi5T6k2BJcmvrVAQ+Dps/kIqhyQEThJE2XazVpVtksTbwhNgSbwtVtmW7uHKOgGPpihb+poYgrheFqEJ/QQBtkkQW0vbWoSq2BZBgJ1k9W9LFUTZGUK17IykM21dDu2iakX+AmwX/ff+QSua1lX/rpIsibK9hdVMdevJYitlS6JsJtungnZ6qEJtFkCKmQIozBKuIFqwZhaROf3Mj9FuTiqZiFo3QYNwJyYLuo0SJ4uwSCJd4bhSNW5JCCuKbW1JWuJd7UrevsJvIamIX59AAmwh+YhcPVuzn3g9wxBqi6JsaXeCaFo4sEkar6WxVCP8VBTW+5xvv1Y7ySJ8P130b7OD1u+pevqF5HlIvwEpYYzweTuFr4XTGm5Fbdj5SJOQTgUAIDSqFOYHXZ9VWBBbl1QNpgh/AgAAF1Uyg9vxKpn+2f6iGtj5iAOnAgBQLDszsoKu35tZIaauIqdqNDRmKnTTZgIAQKyz83BwO76jmPXRBux85IkJTcX27dtp2LBh1KhRI0pLS6MmTZrQmDFjqEjKsQwAKHU2VsqmbRUqB1y/oFHj2LrqiLUFAAAv5m+sSyfz5WfNdoeJvlnbILauGOx8xIkJp+K3334jp9NJb775Jm3YsIFefvllmjhxIj300EPlfWoAJAwvtutJe9O9n1Txc/yvmjWntTVrU0zBuh/dBQAAEoC8oiR69KOOdCLPW1dTaDPTc990oN1HY2umAnY+8sRE+NPFF1+sFheNGzemzZs304QJE+iFF14IeX/ZFitV9BIK+Vf2lTAL4RIWQYWV5FM+2vd1IKF2so/4VG0rtOUJ4tt8qyDeFiqS2pIE8bYgypaE2oZD8EF1K29LUSSS4Ey6hxPbSljhVFKCa4okSVOULYrrhDazIC42S1WxhTZJgJ2S5C+mTEvyF2WnWm1albEzrd5tpkyDnr3gPGqzP4caHDtGBVYr/d6wCh3LSKNsOhGyKLu6RaiULfUz+7//GpZM7/ck/E6Kj7XVEWrDqYhVsirmkyX9zGd8wpzmtd7mI9xm7GarnvhUbBNOQlfQLQrEJdGrXptFEGWbBcGsJNQ2Ofyvgcnu1KrQLOLzPsQq1roVsCXRr+b+pOspCbDFz1ETcZzTragtirf19mdyWIpVPZukCuXJ/olgNh2uQze8nU29W+ymupVP0qFTaTT/j0Z0siCFTCnBt6Uk/2MYwj2HM1m450jyb3P4JJFxhDruw85HnJhwKiSOHz9OVapUCdqnsLBQLS5OnPC/iQEA6MO1KH6pXUstTKWkwBmhohnDaZAhphrz6QenIuqBnQeglH9Tdit9t77hmQbJIYkBYOcjT0yEP/ny559/0vjx4+m2224L2m/s2LGUlZXlXurVqxexcwQARDEswNZdQFQDOw8AEIGdTyyn4sEHHySTyRR0YT2FJ3v27FGhUNdeey0NHz486P5Hjx6tZjRcy65du8r4HQEAYuYJluYCohvYeQCABOx85CnXOa17772XbrzxxqB9WD/hYu/evdSnTx/q3r07TZo0qdj9p6SkqMU3lOHkKe+nj6eE2NBch6BlENryhaJABXbvmPVCm38Me5HNP3OVTdAx2G2GVptDiHl12Bwl1k+ImgqnnqbCgKZCS1NhErQCZNVrk7QXDkFTYRf0E1KbTfg+Flm924qE7QoFzUaBcB55guYhV2g7JbyvVOE6+WooTvz1m9YNV7IbhVqzEHbyf38gughk5x153pogZ563rXLmCzocIfONs0CI4xbaTEIKf6mNhKSFpiLheyvYebIJcfeC7Tfs/u/NLBWwE8Y+kzDOSf20NRU+cfCGKFrQq0BqCEXeDLGKYXRrKsQCseI1lvrZi29zCl8yoc0whH6G8LsQbKVJqlbnq+1Q9xz+19hul+5r/H97jiLvD8NRdLqOBux89FKuTkX16tXVogPPULBD0bFjR5oyZQqZJXFWMZw8eVoE2qrTvpC3BQBEP/wb51DHQCQnJ1PNmjXpp/3faO+T+/N2IDZw2fn1Q14r71MBIH45Vn6Hhp2PXkxGDCgR2aHo3bs3NWjQgN577z2yeGSs4AFfF05Ly7Md/Jbr16+vwqEqVqxIiQCL1FlTkijvOdHeb6K/5507d6pwydq1axf7wKGgoCCkGjfsUKSmppbC2YJIADufODYgkW1eorxnz/dboUIF5VDAzkcvMSHpnzNnjhJn81K3bl2vdaH4RHyzwdu7skDxDzIRfpSeJNp7TrT3m6jvmWcndN8zOwhwEuIX2PnEswGJ9n4T8T273m+wmWhPYOfLh5jI/sS6C3YepAUAAAAAAABQvsSEUwEAAAAAAACIXhLSqeBMIWPGjPHKGBLvJNp7TrT3y+A9A4DfQyLZPdi8+CcRP+NYJiaE2gAAAAAAAIDoJSFnKgAAAAAAAAClB5wKAAAAAAAAQFjAqQAAAAAAAACEBZwKAAAAAAAAQFgkvFOxfft2GjZsGDVq1IjS0tKoSZMmKtNAKBV3Y43//ve/1L17d0pPT6dKlSpRPPL6669Tw4YNVQGcrl270vLlyyleWbRoEV1++eWqyihXlf78888pnhk7dix17txZVVetUaMGXXnllbR58+byPi0QpSSijWdg5+ML2HnY+Vgg4Z2K3377jZxOJ7355pu0YcMGevnll2nixIn00EMPUbzCg+m1115LI0aMoHhkxowZdM8996gbh9WrV1Pbtm2pf//+dODAAYpHcnNz1XtkRyoR+OGHH2jkyJH0888/05w5c8hms9FFF12krgMAviSijWdg5+ML2HnY+ZiAU8oCb5577jmjUaNGcX9ZpkyZYmRlZRnxRpcuXYyRI0e6XzscDqN27drG2LFjjXiHf9KfffaZkUgcOHBAve8ffvihvE8FxAiJYuMZ2Pn4A3YeRCsJP1Mhcfz4capSpUrEHTxQOk/nVq1aRf369XO3mc1m9Xrp0qW4xHH6e2XwmwWhfGfwfYldYOcTD9j52ABOhQ9//vknjR8/nm677bby+URAWBw6dIgcDgdlZ2d7tfPr/fv34+rGGRzWctddd1GPHj2odevW5X06IAaAjY99YOcTC9j52CFunYoHH3xQiVaDLRxr68mePXvo4osvVnqD4cOHU7y/XwBiHdZWrF+/nqZPn17epwIiTKLZeAZ2HiQisPOxg5XilHvvvZduvPHGoH0aN27s/vfevXupT58+KivSpEmTKN7fb7xSrVo1slgslJOT49XOr2vWrFlu5wVKn1GjRtGsWbNUVpS6deviEicYiWbjGdj508DOJw6w87FF3DoV1atXV4sO/PSKB5uOHTvSlClTVAx+PL/feCY5OVl9jvPmzVOpRl1Tp/yajROIfVinePvtt9Nnn31GCxcuVKlCQeKRaDaegZ0/Dex8/AM7H5vErVOhCw82vXv3pgYNGtALL7xABw8edK+L1yfbO3fupCNHjqi/rD9Yu3atam/atCllZmZSrMPpZIcOHUqdOnWiLl260Lhx41Q6vptuuonikVOnTqk4cRfbtm1TnykLUevXr0/xOBU+bdo0+uKLL1StCpdWJisrS9UhACDRbTwDOx9fwM7DzscERoLD6fb4MkhLvDJ06FDx/S5YsMCIF8aPH2/Ur1/fSE5OVilmf/75ZyNe4c9N+jz5c45HAv1e+bcMgC+JaOMZ2Pn4AnYedj4WMPH/ytuxAQAAAAAAAMQusRlYCgAAAAAAAIga4FQAAAAAAAAAwgJOBQAAAAAAACAs4FQAAAAAAAAAwgJOBQAAAAAAACAs4FQAAAAAAAAAwgJOBQAAAAAAACAs4FQAAAAAAAAAwgJOBQAAAAAAACAs4FSAuGbhwoXUoUMHSklJoaZNm9K7775b3qcEAACgFIGdByA6gFMB4pZt27bRgAEDqE+fPrR27Vq666676JZbbqHvvvuuvE8NAABAKQA7D0D0YDIMwyjvkwCgJBw8eJDatGlDd9xxBz300EOqbcmSJdS7d2/69ttv6fvvv6evv/6a1q9f795m0KBBdOzYMZo9ezYuOgAARDmw8wDEDpipADFL9erVafLkyfTYY4/RypUr6eTJk3TDDTfQqFGjqG/fvrR06VLq16+f1zb9+/dX7QAAAKIf2HkAYgdreZ8AAOFw6aWX0vDhw2nw4MHUqVMnysjIoLFjx6p1+/fvp+zsbK/+/PrEiROUn59PaWlpuPgAABDlwM4DEBtgpgLEPC+88ALZ7XaaOXMmTZ06VYmyAQAAxA+w8wBEP3AqQMyzZcsW2rt3LzmdTtq+fbu7vWbNmpSTk+PVl19XrFgRsxQAABBDwM4DEP0g/AnENEVFRXT99dfTwIEDqXnz5iq707p166hGjRrUrVs3+uabb7z6z5kzR7UDAACIDWDnAYgNkP0JxDT3338/ffzxx/TLL79QZmYm9erVi7KysmjWrFkq1WDr1q1p5MiRdPPNN9P8+fNVpijOCMWCbQAAANEP7DwAsQGcChDTBY8uvPBCWrBgAfXs2VO1cfhT27Zt6ZlnnqERI0aoPnfffTdt3LiR6tatS4888gjdeOON5X3qAAAANICdByB2gFMBAAAAAAAACAsItQEAAAAAAABhAacCAAAAAAAAEBZwKgAAAAAAAABhAacCAAAAAAAAEBZwKgAAAAAAAABhAacCAAAAAAAAEBZwKgAAAAAAAABhAacCAAAAAAAAEBZwKgAAAAAAAABhAacCAAAAAAAAEBZwKgAAAAAAAAAUDv8P/FTihu8YKpIAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = visualize_model(\n", + " model, vocs, data, variable_names=[\"x0\", \"x1\"], reference_point=data.iloc[-1][vocs.variable_names].to_dict()\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'x0': 0.8296234531171609, 'x1': 1.3067972159405206, 'x2': 1.7292349786123893}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data.iloc[-1][vocs.variable_names].to_dict()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using Custom Kernels via covar_modules\n", + "\n", + "The `covar_modules` parameter in `StandardModelConstructor` allows you to specify custom GPyTorch kernels for specific outputs. This is useful when you have domain knowledge about the function structure." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Custom model with Matern kernel:\n", + "Kernel type: ScaleKernel(\n", + " (base_kernel): MaternKernel(\n", + " (raw_lengthscale_constraint): Positive()\n", + " )\n", + " (raw_outputscale_constraint): Positive()\n", + ")\n", + "Nu parameter: 2.5\n" + ] + } + ], + "source": [ + "from gpytorch.kernels import MaternKernel, ScaleKernel\n", + "\n", + "# Example 1: Use a specific kernel for the objective\n", + "# Here we use a Matern kernel with nu=2.5 for the objective \"y\"\n", + "custom_covar_modules = {\n", + " \"y\": ScaleKernel(MaternKernel(nu=2.5)) # Matern 5/2 kernel for objective\n", + "}\n", + "\n", + "model_constructor_custom = StandardModelConstructor(\n", + " covar_modules=custom_covar_modules, use_low_noise_prior=True\n", + ")\n", + "\n", + "# Build model with custom kernel\n", + "custom_model = model_constructor_custom.build_model_from_vocs(vocs=vocs, data=data)\n", + "\n", + "print(\"Custom model with Matern kernel:\")\n", + "print(f\"Kernel type: {custom_model.models[0].covar_module}\")\n", + "print(f\"Nu parameter: {custom_model.models[0].covar_module.base_kernel.nu}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "xopt-dev", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/xopt/generators/bayesian/base_model.py b/xopt/generators/bayesian/base_model.py index 92e9336ab..c50b1b982 100644 --- a/xopt/generators/bayesian/base_model.py +++ b/xopt/generators/bayesian/base_model.py @@ -5,12 +5,13 @@ from botorch.exceptions import ModelFittingError import pandas as pd import torch -from botorch import fit_gpytorch_mll -from botorch.models import ModelListGP, SingleTaskGP +from botorch import fit_fully_bayesian_model_nuts, fit_gpytorch_mll +from botorch.models import ModelListGP, SaasFullyBayesianSingleTaskGP, SingleTaskGP from botorch.models.model import Model from gpytorch import ExactMarginalLogLikelihood from pydantic import ConfigDict from torch import Tensor +from botorch.models.map_saas import get_map_saas_model from xopt.generators.bayesian.custom_botorch.heteroskedastic import ( XoptHeteroskedasticSingleTaskGP, @@ -212,3 +213,112 @@ def build_heteroskedastic_gp( "Model fitting failed for heteroskedastic GP. Returning untrained model." ) return model + + @staticmethod + def build_saas_gp( + X: Tensor, Y: Tensor, Yvar: Tensor, train: bool = True, **kwargs + ) -> Model: + """ + Utility method for creating and training a fully Bayesian SAAS SingleTaskGP model. + + Parameters + ---------- + X : Tensor + Training data for input variables. + Y : Tensor + Training data for outcome variables. + Yvar : Tensor + Training data for outcome variable variances. + train : bool, True + Flag to specify if hyperparameter training should take place + **kwargs + Additional keyword arguments for model configuration. + + Returns + ------- + Model + The trained SAAS SingleTaskGP model. + + Notes + ----- + SAAS modeling can be unstable when the number of dimensions is high and the amount of data is low. + Your results may vary and keep an eye on warnings. + + """ + WARMUP_STEPS = 512 + NUM_SAMPLES = 256 + THINNING = 16 + + if X.shape[0] == 0 or Y.shape[0] == 0: + raise ValueError("no data found to train model!") + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + model = SaasFullyBayesianSingleTaskGP(X, Y, Yvar, **kwargs) + + if train: + try: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + fit_fully_bayesian_model_nuts( + model, + warmup_steps=WARMUP_STEPS, + num_samples=NUM_SAMPLES, + thinning=THINNING, + disable_progbar=True, + ) + except ModelFittingError: + warnings.warn( + "Model fitting failed for SAAS GP. Returning untrained model." + ) + return model + + @staticmethod + def build_map_saas_gp( + X: Tensor, Y: Tensor, Yvar: Tensor, train: bool = True, **kwargs + ): + """ + Utility method for creating and training a MAP SAAS SingleTaskGP model. + + Parameters + ---------- + X : Tensor + Training data for input variables. + Y : Tensor + Training data for outcome variables. + Yvar : Tensor + Training data for outcome variable variances. + train : bool, True + Flag to specify if hyperparameter training should take place + **kwargs + Additional keyword arguments for model configuration. + + Returns + ------- + Model + The trained MAP SAAS SingleTaskGP model. + + Notes + ----- + MAP SAAS modeling can be unstable when the number of dimensions is high and the amount of data is low. + Your results may vary and keep an eye on warnings. + + """ + + if X.shape[0] == 0 or Y.shape[0] == 0: + raise ValueError("no data found to train model!") + + model = get_map_saas_model(X, Y, Yvar, **kwargs) + + if train: + try: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + mll = ExactMarginalLogLikelihood(model.likelihood, model) + fit_gpytorch_mll(mll) + except ModelFittingError: + warnings.warn( + "Model fitting failed for MAP SAAS GP. Returning untrained model." + ) + + return model diff --git a/xopt/generators/bayesian/models/__init__.py b/xopt/generators/bayesian/models/__init__.py index 17faada6a..4683816d5 100644 --- a/xopt/generators/bayesian/models/__init__.py +++ b/xopt/generators/bayesian/models/__init__.py @@ -1,5 +1,11 @@ from xopt.generators.bayesian.models.standard import StandardModelConstructor from xopt.generators.bayesian.models.time_dependent import TimeDependentModelConstructor from xopt.generators.bayesian.models.prior_mean import CustomMean +from xopt.generators.bayesian.models.saas import SaasModelConstructor -__all__ = ["StandardModelConstructor", "TimeDependentModelConstructor", "CustomMean"] +__all__ = [ + "StandardModelConstructor", + "TimeDependentModelConstructor", + "CustomMean", + "SaasModelConstructor", +] diff --git a/xopt/generators/bayesian/models/saas.py b/xopt/generators/bayesian/models/saas.py new file mode 100644 index 000000000..9ff1d6cc0 --- /dev/null +++ b/xopt/generators/bayesian/models/saas.py @@ -0,0 +1,465 @@ +import os.path +import warnings +from copy import deepcopy +from functools import partial +from typing import Any, Dict, List, Literal, Optional, Union, cast + +from botorch.exceptions import ModelFittingError +import botorch.settings +import pandas as pd +import torch +from botorch import fit_fully_bayesian_model_nuts, fit_gpytorch_mll +from botorch.models import ModelListGP, SingleTaskGP +from botorch.models.gpytorch import BatchedMultiOutputGPyTorchModel +from botorch.models.transforms import Normalize, Standardize +from botorch.optim import ExpMAStoppingCriterion +from botorch.optim.fit import fit_gpytorch_mll_scipy, fit_gpytorch_mll_torch +from gpytorch import ExactMarginalLogLikelihood +from gpytorch.constraints import GreaterThan +from gpytorch.kernels import Kernel +from gpytorch.likelihoods import GaussianLikelihood, Likelihood +from gpytorch.likelihoods.gaussian_likelihood import FixedNoiseGaussianLikelihood +from gpytorch.priors import GammaPrior, Prior +from pydantic import ConfigDict, Field, field_validator +from pydantic_core.core_schema import ValidationInfo +from torch.nn import Module +from torch.optim import Adam + +from xopt.generators.bayesian.base_model import ModelConstructor +from xopt.generators.bayesian.models.prior_mean import CustomMean +from xopt.generators.bayesian.utils import get_training_data, get_training_data_batched +from xopt.pydantic import XoptBaseModel, decode_torch_module + +DECODERS = {"torch.float32": torch.float32, "torch.float64": torch.float64} +MIN_INFERRED_NOISE_LEVEL = 1e-4 + + +class SaasModelConstructor(ModelConstructor): + """ + A class for constructing Sparse Axis-Aligned Subspace (SAAS) models. + + Attributes + ---------- + name : str + The name of the model (frozen). + + use_low_noise_prior : bool + Specify if the model should assume a low noise environment. + + trainable_mean_keys : List[str] + List of prior mean modules that can be trained. + + transform_inputs : Union[Dict[str, bool], bool] + Specify if inputs should be transformed inside the GP model. Can optionally + specify a dict of specifications. + + custom_noise_prior : Optional[Prior] + Specify a custom noise prior for the GP likelihood. Overwrites value specified + by use_low_noise_prior. + + use_cached_hyperparameters : Optional[bool] + Flag to specify if cached hyperparameters should be used in model creation. + Training will still occur unless train_model is False. + + train_method : Literal["lbfgs", "adam"] + Numerical optimization algorithm to use. + + train_model : bool + Flag to specify if the model should be trained (fitted to data). + + train_config : NumericalOptimizerConfig + Configuration of the numerical optimizer. + + """ + + name: str = Field("saas", frozen=True) + use_low_noise_prior: bool = Field( + False, description="specify if model should assume a low noise environment" + ) + trainable_mean_keys: List[str] = Field( + [], description="list of prior mean modules that can be trained" + ) + transform_inputs: Union[Dict[str, bool], bool] = Field( + True, + description="specify if inputs should be transformed inside the gp " + "model, can optionally specify a dict of specifications", + ) + custom_noise_prior: Optional[Prior] = Field( + None, + description="specify custom noise prior for the GP likelihood, " + "overwrites value specified by use_low_noise_prior", + ) + use_cached_hyperparameters: Optional[bool] = Field( + False, + description="flag to specify if cached hyperparameters should be used in " + "model creation. Training will still occur unless train_model is False.", + ) + train_model: bool = Field( + True, + description="flag to specify if the model should be trained (fitted to data)", + ) + warmup_steps: int = Field( + 512, description="number of warmup steps to use if training with MCMC" + ) + num_samples: int = Field( + 256, description="number of samples to use if training with MCMC" + ) + thinning: int = Field( + 16, + description="thinning factor to use if training with MCMC, only every nth sample is kept", + ) + + _hyperparameter_store: Optional[Dict] = None + + model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) + + def __init__(self, **kwargs: Any): + super().__init__(**kwargs) + + @field_validator("trainable_mean_keys") + def validate_trainable_mean_keys(cls, value: Any, info: ValidationInfo): + return value + + def get_likelihood( + self, + batch_shape: torch.Size = torch.Size(), + ) -> Likelihood: + """ + Get the likelihood for the model, considering the low noise prior and or a + custom noise prior. + + Returns + ------- + Likelihood + The likelihood for the model. + + """ + if self.custom_noise_prior is not None: + likelihood = GaussianLikelihood( + noise_prior=self.custom_noise_prior, batch_shape=batch_shape + ) + elif self.use_low_noise_prior: + likelihood = GaussianLikelihood( + noise_prior=GammaPrior(1.0, 100.0), batch_shape=batch_shape + ) + else: + noise_prior = GammaPrior(1.1, 0.05) + noise_prior_mode = (noise_prior.concentration - 1) / noise_prior.rate + likelihood = GaussianLikelihood( + noise_prior=noise_prior, + noise_constraint=GreaterThan( + MIN_INFERRED_NOISE_LEVEL, + transform=None, + initial_value=noise_prior_mode, + ), + batch_shape=batch_shape, + ) + return likelihood + + def build_model( + self, + input_names: List[str], + outcome_names: List[str], + data: pd.DataFrame, + input_bounds: Dict[str, List] = None, + dtype: torch.dtype = torch.double, + device: Union[torch.device, str] = "cpu", + ) -> ModelListGP: + """ + Construct independent models for each objective and constraint. + + Parameters + ---------- + input_names : List[str] + Names of input variables. + outcome_names : List[str] + Names of outcome variables. + data : pd.DataFrame + Data used for training the model. + input_bounds : Dict[str, List], optional + Bounds for input variables. + dtype : torch.dtype, optional + Data type for the model (default is torch.double). + device : Union[torch.device, str], optional + Device on which to perform computations (default is "cpu"). + + Returns + ------- + ModelListGP + A list of trained botorch models. + + """ + # build model + tkwargs = {"dtype": dtype, "device": device} + models = [] + + # validate if model caching can be used if requested + if self.use_cached_hyperparameters: + if self._hyperparameter_store is None: + raise RuntimeWarning( + "cannot use cached hyperparameters, hyperparameter store empty, " + "training GP model hyperparameters instead" + ) + + for outcome_name in outcome_names: + input_transform = self._get_input_transform( + outcome_name, input_names, input_bounds, tkwargs + ) + outcome_transform = Standardize(1) + + # get training data + train_X, train_Y, train_Yvar = get_training_data( + input_names, outcome_name, data + ) + # collect arguments into a single dict + kwargs = { + "input_transform": input_transform, + "outcome_transform": outcome_transform, + } + + # train SAAS single-task-gp + models.append( + self.build_saas_gp( + train_X.to(**tkwargs), + train_Y.to(**tkwargs), + train_Yvar.to(**tkwargs) if train_Yvar is not None else None, + train=False, + **kwargs, + ) + ) + + full_model = ModelListGP(*models) + + # if specified, use cached model hyperparameters + if self.use_cached_hyperparameters and self._hyperparameter_store is not None: + store = { + name: ele.to(**tkwargs) + for name, ele in self._hyperparameter_store.items() + } + full_model.load_state_dict(store) + + if self.train_model: + full_model = self._train_model(full_model) + + # cache model hyperparameters + self._hyperparameter_store = full_model.state_dict() + + return full_model.to(**tkwargs) + + def _train_model(self, model): + models = model.models if isinstance(model, ModelListGP) else [model] + + for m in models: + try: + fit_fully_bayesian_model_nuts( + m, + warmup_steps=self.warmup_steps, + num_samples=self.num_samples, + thinning=self.thinning, + disable_progbar=True, + ) + except ModelFittingError: + warnings.warn("Model fitting failed. Returning untrained model.") + return model + + @staticmethod + def _get_module(base, name): + """ + Get the module for a given name. + + Parameters + ---------- + base : Union[Module, Dict[str, Module]] + The base module or a dictionary of modules. + name : str + The name of the module. + + Returns + ------- + Module + The retrieved module. + + """ + if isinstance(base, Module): + return deepcopy(base) + elif isinstance(base, dict): + return deepcopy(base.pop(name, None)) + else: + return None + + def _get_input_transform(self, outcome_name, input_names, input_bounds, tkwargs): + """ + Get input transform based on the supplied bounds and attributes + + Parameters + ---------- + outcome_name : str + The name of the outcome variable. + input_names : list[str] + The names of the input variables. + input_bounds : dict[str, tuple[float, float]] + The bounds for the input variables. + tkwargs : dict + Additional keyword arguments for tensor creation. + + """ + # get input bounds + if input_bounds is None: + bounds = None + else: + bounds = torch.vstack( + [torch.tensor(input_bounds[name], **tkwargs) for name in input_names] + ).T + + # create transform + input_transform = Normalize(len(input_names), bounds=bounds) + + # remove input transform if the bool is False or the dict entry is false + if isinstance(self.transform_inputs, bool): + if not self.transform_inputs: + input_transform = None + if ( + isinstance(self.transform_inputs, dict) + and outcome_name in self.transform_inputs + ): + if not self.transform_inputs[outcome_name]: + input_transform = None + + # remove warnings if input transform is None + if input_transform is None: + botorch.settings.validate_input_scaling(False) + + return input_transform + + +class BatchedSaasModelConstructor(SaasModelConstructor): + """ + BatchedModelConstructor treats outputs as an additional dimension instead of looping over them. + It is useful when multiple outputs are being modelled and their settings are similar. + + A batch shares training points (i.e. train_X/train_Y) and kernel. It uses a single pytorch + graph (1 sum loss), which changes convergence criteria. Resulting GP parameters are expected + to differ slightly from individual fitting. + + Batch modelling is faster on GPUs, especially in the intermediate (<~1000) problem sizes where GPU + call overhead is significant and all models converge in roughly equal number of steps. + + On CPU, batched fitting performance varies from slightly useful to slightly detrimental - mostly + not worth it. + + See benchmarking docs on how to run tests for your specific hardware. + """ + + def _get_input_transform( + self, + outcome_names: list[str], + input_names: list[str], + input_bounds, + batch_shape: torch.Size = torch.Size(), + ) -> Optional[Normalize]: + if input_bounds is None: + bounds = None + else: + bounds = torch.vstack( + [torch.tensor(input_bounds[name]) for name in input_names] + ).T + + input_transform = Normalize( + len(input_names), + bounds=bounds, + batch_shape=batch_shape, + ) + + # remove input transform if the bool is False or the dict entry is false + if isinstance(self.transform_inputs, bool): + if not self.transform_inputs: + input_transform = None + + if isinstance(self.transform_inputs, dict): + raise AttributeError( + "Cannot specify dict for transform_inputs when using BatchedModelConstructor" + ) + + if input_transform is None: + botorch.settings.validate_input_scaling(False) + + return input_transform + + def build_model( + self, + input_names: List[str], + outcome_names: List[str], + data: pd.DataFrame, + input_bounds: Dict[str, List] = None, + dtype: torch.dtype = torch.double, + device: Union[torch.device, str] = "cpu", + ) -> SingleTaskGP: + """ + Construct a single batched model for all objectives and constraints. + """ + tkwargs = {"dtype": dtype, "device": device} + + if self.use_cached_hyperparameters: + if self._hyperparameter_store is None: + raise RuntimeWarning( + "cannot use cached hyperparameters, hyperparameter store empty, " + "training GP model hyperparameters instead" + ) + + train_X, train_Y, train_Yvar = get_training_data_batched( + input_names, outcome_names, data + ) + if train_X.shape[0] == 0 or train_Y.shape[0] == 0: + raise ValueError("no data found to train model!") + + # train_Y is n x m, will get transformed to (m) x n x 1 by + # SingleTaskGP to run as m independent batches + # _input_batch_shape = empty + # _aug_batch_shape = (m,) = (_num_outputs,) + + _num_outputs = train_Y.shape[-1] + _input_batch_shape, _aug_batch_shape = ( + BatchedMultiOutputGPyTorchModel.get_batch_dimensions( + train_X=train_X, train_Y=train_Y + ) + ) + # input and output transforms are applied BEFORE tensors are unrolled + input_transform = self._get_input_transform( + outcome_names, + input_names, + input_bounds=input_bounds, + batch_shape=_input_batch_shape, + ) + outcome_transform = Standardize( + m=train_Y.shape[-1], batch_shape=_input_batch_shape + ) + kwargs = { + "input_transform": input_transform, + "outcome_transform": outcome_transform, + } + + if train_Yvar is None: + likelihood = self.get_likelihood(batch_shape=_aug_batch_shape) + else: + likelihood = FixedNoiseGaussianLikelihood( + noise=train_Yvar, batch_shape=_aug_batch_shape + ) + full_model = SingleTaskGP( + train_X, train_Y, train_Yvar=train_Yvar, likelihood=likelihood, **kwargs + ) + full_model.to(**tkwargs) + + if self.use_cached_hyperparameters and self._hyperparameter_store is not None: + store = { + name: ele.to(**tkwargs) + for name, ele in self._hyperparameter_store.items() + } + full_model.load_state_dict(store) + + if self.train_model: + full_model = self._train_model(full_model) + + # cache model hyperparameters + self._hyperparameter_store = full_model.state_dict() + + return full_model diff --git a/xopt/generators/bayesian/models/standard.py b/xopt/generators/bayesian/models/standard.py index 6a1949556..d4c757271 100644 --- a/xopt/generators/bayesian/models/standard.py +++ b/xopt/generators/bayesian/models/standard.py @@ -130,6 +130,9 @@ class StandardModelConstructor(ModelConstructor): description="specify if inputs should be transformed inside the gp " "model, can optionally specify a dict of specifications", ) + saas_outputs: List[str] = Field( + [], description="list of output names to apply SAAS priors to" + ) custom_noise_prior: Optional[Prior] = Field( None, description="specify custom noise prior for the GP likelihood, " @@ -335,6 +338,31 @@ def build_model( "mean_module": mean_module, } + # handle saas model construction + if outcome_name in self.saas_outputs: + if kwargs.pop("covar_module", None) is not None: + warnings.warn( + f"Covariance module specified for output {outcome_name} will be overwritten by SAAS model construction." + ) + if kwargs.pop("mean_module", None) is not None: + warnings.warn( + f"Mean module specified for output {outcome_name} will be overwritten by SAAS model construction." + ) + models.append( + self.build_map_saas_gp( + train_X.to(**tkwargs), + train_Y.to(**tkwargs), + train_Yvar.to(**tkwargs) if train_Yvar is not None else None, + train=False, + **kwargs, + ) + ) + + # add in likelihood if needed + # if models[-1].likelihood is None: + # models[-1].likelihood = self.get_likelihood() + continue + if train_Yvar is None: # train basic single-task-gp model models.append( diff --git a/xopt/tests/generators/bayesian/test_model_constructor.py b/xopt/tests/generators/bayesian/test_model_constructor.py index 2c242f23c..968add4fb 100644 --- a/xopt/tests/generators/bayesian/test_model_constructor.py +++ b/xopt/tests/generators/bayesian/test_model_constructor.py @@ -10,7 +10,7 @@ import yaml from botorch import fit_gpytorch_mll from botorch.exceptions import ModelFittingError -from botorch.models import SingleTaskGP +from botorch.models import ModelListGP, SingleTaskGP from botorch.models.transforms import Normalize, Standardize from botorch.optim.fit import fit_gpytorch_mll_torch from gpytorch import ExactMarginalLogLikelihood @@ -32,6 +32,7 @@ LBFGSNumericalOptimizerConfig, StandardModelConstructor, ) +from xopt.generators.bayesian.models import SaasModelConstructor from xopt.generators.bayesian.utils import get_training_data_batched from xopt.resources.testing import ( TEST_VOCS_BASE, @@ -1148,3 +1149,11 @@ def test_batched_empty_data(self): constructor = BatchedModelConstructor() with pytest.raises(ValueError, match="no data found"): constructor.build_model_from_vocs(test_vocs, empty_data) + + def test_saas_model_constructor(self): + test_vocs = deepcopy(TEST_VOCS) + test_data = deepcopy(TEST_DATA) + + constructor = SaasModelConstructor(warmup_steps=2, num_samples=4, thinning=2) + model = constructor.build_model_from_vocs(test_vocs, test_data) + assert isinstance(model, ModelListGP) From 66ae3dbd60d9fadf8ffa86d959faa6167853b6b8 Mon Sep 17 00:00:00 2001 From: Ryan Roussel Date: Thu, 16 Apr 2026 14:18:25 -0500 Subject: [PATCH 02/16] add back in map saas builder --- xopt/generators/bayesian/base_model.py | 50 ++++++++++++++ xopt/generators/bayesian/models/saas.py | 90 +------------------------ 2 files changed, 52 insertions(+), 88 deletions(-) diff --git a/xopt/generators/bayesian/base_model.py b/xopt/generators/bayesian/base_model.py index 74738fb85..6a864cf54 100644 --- a/xopt/generators/bayesian/base_model.py +++ b/xopt/generators/bayesian/base_model.py @@ -249,3 +249,53 @@ def build_approximate_gp( fit_gpytorch_mll(mll) return model + + @staticmethod + def build_map_saas_gp( + X: Tensor, Y: Tensor, Yvar: Tensor, train: bool = True, **kwargs + ): + """ + Utility method for creating and training a MAP SAAS SingleTaskGP model. + + Parameters + ---------- + X : Tensor + Training data for input variables. + Y : Tensor + Training data for outcome variables. + Yvar : Tensor + Training data for outcome variable variances. + train : bool, True + Flag to specify if hyperparameter training should take place + **kwargs + Additional keyword arguments for model configuration. + + Returns + ------- + Model + The trained MAP SAAS SingleTaskGP model. + + Notes + ----- + MAP SAAS modeling can be unstable when the number of dimensions is high and the amount of data is low. + Your results may vary and keep an eye on warnings. + + """ + + if X.shape[0] == 0 or Y.shape[0] == 0: + raise ValueError("no data found to train model!") + + model = get_map_saas_model(X, Y, Yvar, **kwargs) + + if train: + try: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + mll = ExactMarginalLogLikelihood(model.likelihood, model) + fit_gpytorch_mll(mll) + except ModelFittingError: + warnings.warn( + "Model fitting failed for MAP SAAS GP. Returning untrained model." + ) + + return model diff --git a/xopt/generators/bayesian/models/saas.py b/xopt/generators/bayesian/models/saas.py index 9ff1d6cc0..5ca418128 100644 --- a/xopt/generators/bayesian/models/saas.py +++ b/xopt/generators/bayesian/models/saas.py @@ -25,16 +25,15 @@ from torch.nn import Module from torch.optim import Adam -from xopt.generators.bayesian.base_model import ModelConstructor +from xopt.generators.bayesian.models.standard import StandardModelConstructor from xopt.generators.bayesian.models.prior_mean import CustomMean from xopt.generators.bayesian.utils import get_training_data, get_training_data_batched -from xopt.pydantic import XoptBaseModel, decode_torch_module DECODERS = {"torch.float32": torch.float32, "torch.float64": torch.float64} MIN_INFERRED_NOISE_LEVEL = 1e-4 -class SaasModelConstructor(ModelConstructor): +class SaasModelConstructor(StandardModelConstructor): """ A class for constructing Sparse Axis-Aligned Subspace (SAAS) models. @@ -246,91 +245,6 @@ def build_model( return full_model.to(**tkwargs) - def _train_model(self, model): - models = model.models if isinstance(model, ModelListGP) else [model] - - for m in models: - try: - fit_fully_bayesian_model_nuts( - m, - warmup_steps=self.warmup_steps, - num_samples=self.num_samples, - thinning=self.thinning, - disable_progbar=True, - ) - except ModelFittingError: - warnings.warn("Model fitting failed. Returning untrained model.") - return model - - @staticmethod - def _get_module(base, name): - """ - Get the module for a given name. - - Parameters - ---------- - base : Union[Module, Dict[str, Module]] - The base module or a dictionary of modules. - name : str - The name of the module. - - Returns - ------- - Module - The retrieved module. - - """ - if isinstance(base, Module): - return deepcopy(base) - elif isinstance(base, dict): - return deepcopy(base.pop(name, None)) - else: - return None - - def _get_input_transform(self, outcome_name, input_names, input_bounds, tkwargs): - """ - Get input transform based on the supplied bounds and attributes - - Parameters - ---------- - outcome_name : str - The name of the outcome variable. - input_names : list[str] - The names of the input variables. - input_bounds : dict[str, tuple[float, float]] - The bounds for the input variables. - tkwargs : dict - Additional keyword arguments for tensor creation. - - """ - # get input bounds - if input_bounds is None: - bounds = None - else: - bounds = torch.vstack( - [torch.tensor(input_bounds[name], **tkwargs) for name in input_names] - ).T - - # create transform - input_transform = Normalize(len(input_names), bounds=bounds) - - # remove input transform if the bool is False or the dict entry is false - if isinstance(self.transform_inputs, bool): - if not self.transform_inputs: - input_transform = None - if ( - isinstance(self.transform_inputs, dict) - and outcome_name in self.transform_inputs - ): - if not self.transform_inputs[outcome_name]: - input_transform = None - - # remove warnings if input transform is None - if input_transform is None: - botorch.settings.validate_input_scaling(False) - - return input_transform - class BatchedSaasModelConstructor(SaasModelConstructor): """ From feb0b3be666d453f90754e9832e1c82076768cc9 Mon Sep 17 00:00:00 2001 From: Ryan Roussel Date: Thu, 16 Apr 2026 15:02:13 -0500 Subject: [PATCH 03/16] remove fully bayesian saas --- xopt/generators/bayesian/models/__init__.py | 2 - xopt/generators/bayesian/models/saas.py | 379 ------------------ .../bayesian/test_model_constructor.py | 3 +- 3 files changed, 1 insertion(+), 383 deletions(-) delete mode 100644 xopt/generators/bayesian/models/saas.py diff --git a/xopt/generators/bayesian/models/__init__.py b/xopt/generators/bayesian/models/__init__.py index b01b8bcd8..1edf95adb 100644 --- a/xopt/generators/bayesian/models/__init__.py +++ b/xopt/generators/bayesian/models/__init__.py @@ -4,7 +4,6 @@ ) from xopt.generators.bayesian.models.time_dependent import TimeDependentModelConstructor from xopt.generators.bayesian.models.prior_mean import CustomMean -from xopt.generators.bayesian.models.saas import SaasModelConstructor from xopt.generators.bayesian.models.approximate import ApproximateModelConstructor __all__ = [ @@ -13,5 +12,4 @@ "TimeDependentModelConstructor", "CustomMean", "ApproximateModelConstructor", - "SaasModelConstructor", ] diff --git a/xopt/generators/bayesian/models/saas.py b/xopt/generators/bayesian/models/saas.py deleted file mode 100644 index 5ca418128..000000000 --- a/xopt/generators/bayesian/models/saas.py +++ /dev/null @@ -1,379 +0,0 @@ -import os.path -import warnings -from copy import deepcopy -from functools import partial -from typing import Any, Dict, List, Literal, Optional, Union, cast - -from botorch.exceptions import ModelFittingError -import botorch.settings -import pandas as pd -import torch -from botorch import fit_fully_bayesian_model_nuts, fit_gpytorch_mll -from botorch.models import ModelListGP, SingleTaskGP -from botorch.models.gpytorch import BatchedMultiOutputGPyTorchModel -from botorch.models.transforms import Normalize, Standardize -from botorch.optim import ExpMAStoppingCriterion -from botorch.optim.fit import fit_gpytorch_mll_scipy, fit_gpytorch_mll_torch -from gpytorch import ExactMarginalLogLikelihood -from gpytorch.constraints import GreaterThan -from gpytorch.kernels import Kernel -from gpytorch.likelihoods import GaussianLikelihood, Likelihood -from gpytorch.likelihoods.gaussian_likelihood import FixedNoiseGaussianLikelihood -from gpytorch.priors import GammaPrior, Prior -from pydantic import ConfigDict, Field, field_validator -from pydantic_core.core_schema import ValidationInfo -from torch.nn import Module -from torch.optim import Adam - -from xopt.generators.bayesian.models.standard import StandardModelConstructor -from xopt.generators.bayesian.models.prior_mean import CustomMean -from xopt.generators.bayesian.utils import get_training_data, get_training_data_batched - -DECODERS = {"torch.float32": torch.float32, "torch.float64": torch.float64} -MIN_INFERRED_NOISE_LEVEL = 1e-4 - - -class SaasModelConstructor(StandardModelConstructor): - """ - A class for constructing Sparse Axis-Aligned Subspace (SAAS) models. - - Attributes - ---------- - name : str - The name of the model (frozen). - - use_low_noise_prior : bool - Specify if the model should assume a low noise environment. - - trainable_mean_keys : List[str] - List of prior mean modules that can be trained. - - transform_inputs : Union[Dict[str, bool], bool] - Specify if inputs should be transformed inside the GP model. Can optionally - specify a dict of specifications. - - custom_noise_prior : Optional[Prior] - Specify a custom noise prior for the GP likelihood. Overwrites value specified - by use_low_noise_prior. - - use_cached_hyperparameters : Optional[bool] - Flag to specify if cached hyperparameters should be used in model creation. - Training will still occur unless train_model is False. - - train_method : Literal["lbfgs", "adam"] - Numerical optimization algorithm to use. - - train_model : bool - Flag to specify if the model should be trained (fitted to data). - - train_config : NumericalOptimizerConfig - Configuration of the numerical optimizer. - - """ - - name: str = Field("saas", frozen=True) - use_low_noise_prior: bool = Field( - False, description="specify if model should assume a low noise environment" - ) - trainable_mean_keys: List[str] = Field( - [], description="list of prior mean modules that can be trained" - ) - transform_inputs: Union[Dict[str, bool], bool] = Field( - True, - description="specify if inputs should be transformed inside the gp " - "model, can optionally specify a dict of specifications", - ) - custom_noise_prior: Optional[Prior] = Field( - None, - description="specify custom noise prior for the GP likelihood, " - "overwrites value specified by use_low_noise_prior", - ) - use_cached_hyperparameters: Optional[bool] = Field( - False, - description="flag to specify if cached hyperparameters should be used in " - "model creation. Training will still occur unless train_model is False.", - ) - train_model: bool = Field( - True, - description="flag to specify if the model should be trained (fitted to data)", - ) - warmup_steps: int = Field( - 512, description="number of warmup steps to use if training with MCMC" - ) - num_samples: int = Field( - 256, description="number of samples to use if training with MCMC" - ) - thinning: int = Field( - 16, - description="thinning factor to use if training with MCMC, only every nth sample is kept", - ) - - _hyperparameter_store: Optional[Dict] = None - - model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) - - def __init__(self, **kwargs: Any): - super().__init__(**kwargs) - - @field_validator("trainable_mean_keys") - def validate_trainable_mean_keys(cls, value: Any, info: ValidationInfo): - return value - - def get_likelihood( - self, - batch_shape: torch.Size = torch.Size(), - ) -> Likelihood: - """ - Get the likelihood for the model, considering the low noise prior and or a - custom noise prior. - - Returns - ------- - Likelihood - The likelihood for the model. - - """ - if self.custom_noise_prior is not None: - likelihood = GaussianLikelihood( - noise_prior=self.custom_noise_prior, batch_shape=batch_shape - ) - elif self.use_low_noise_prior: - likelihood = GaussianLikelihood( - noise_prior=GammaPrior(1.0, 100.0), batch_shape=batch_shape - ) - else: - noise_prior = GammaPrior(1.1, 0.05) - noise_prior_mode = (noise_prior.concentration - 1) / noise_prior.rate - likelihood = GaussianLikelihood( - noise_prior=noise_prior, - noise_constraint=GreaterThan( - MIN_INFERRED_NOISE_LEVEL, - transform=None, - initial_value=noise_prior_mode, - ), - batch_shape=batch_shape, - ) - return likelihood - - def build_model( - self, - input_names: List[str], - outcome_names: List[str], - data: pd.DataFrame, - input_bounds: Dict[str, List] = None, - dtype: torch.dtype = torch.double, - device: Union[torch.device, str] = "cpu", - ) -> ModelListGP: - """ - Construct independent models for each objective and constraint. - - Parameters - ---------- - input_names : List[str] - Names of input variables. - outcome_names : List[str] - Names of outcome variables. - data : pd.DataFrame - Data used for training the model. - input_bounds : Dict[str, List], optional - Bounds for input variables. - dtype : torch.dtype, optional - Data type for the model (default is torch.double). - device : Union[torch.device, str], optional - Device on which to perform computations (default is "cpu"). - - Returns - ------- - ModelListGP - A list of trained botorch models. - - """ - # build model - tkwargs = {"dtype": dtype, "device": device} - models = [] - - # validate if model caching can be used if requested - if self.use_cached_hyperparameters: - if self._hyperparameter_store is None: - raise RuntimeWarning( - "cannot use cached hyperparameters, hyperparameter store empty, " - "training GP model hyperparameters instead" - ) - - for outcome_name in outcome_names: - input_transform = self._get_input_transform( - outcome_name, input_names, input_bounds, tkwargs - ) - outcome_transform = Standardize(1) - - # get training data - train_X, train_Y, train_Yvar = get_training_data( - input_names, outcome_name, data - ) - # collect arguments into a single dict - kwargs = { - "input_transform": input_transform, - "outcome_transform": outcome_transform, - } - - # train SAAS single-task-gp - models.append( - self.build_saas_gp( - train_X.to(**tkwargs), - train_Y.to(**tkwargs), - train_Yvar.to(**tkwargs) if train_Yvar is not None else None, - train=False, - **kwargs, - ) - ) - - full_model = ModelListGP(*models) - - # if specified, use cached model hyperparameters - if self.use_cached_hyperparameters and self._hyperparameter_store is not None: - store = { - name: ele.to(**tkwargs) - for name, ele in self._hyperparameter_store.items() - } - full_model.load_state_dict(store) - - if self.train_model: - full_model = self._train_model(full_model) - - # cache model hyperparameters - self._hyperparameter_store = full_model.state_dict() - - return full_model.to(**tkwargs) - - -class BatchedSaasModelConstructor(SaasModelConstructor): - """ - BatchedModelConstructor treats outputs as an additional dimension instead of looping over them. - It is useful when multiple outputs are being modelled and their settings are similar. - - A batch shares training points (i.e. train_X/train_Y) and kernel. It uses a single pytorch - graph (1 sum loss), which changes convergence criteria. Resulting GP parameters are expected - to differ slightly from individual fitting. - - Batch modelling is faster on GPUs, especially in the intermediate (<~1000) problem sizes where GPU - call overhead is significant and all models converge in roughly equal number of steps. - - On CPU, batched fitting performance varies from slightly useful to slightly detrimental - mostly - not worth it. - - See benchmarking docs on how to run tests for your specific hardware. - """ - - def _get_input_transform( - self, - outcome_names: list[str], - input_names: list[str], - input_bounds, - batch_shape: torch.Size = torch.Size(), - ) -> Optional[Normalize]: - if input_bounds is None: - bounds = None - else: - bounds = torch.vstack( - [torch.tensor(input_bounds[name]) for name in input_names] - ).T - - input_transform = Normalize( - len(input_names), - bounds=bounds, - batch_shape=batch_shape, - ) - - # remove input transform if the bool is False or the dict entry is false - if isinstance(self.transform_inputs, bool): - if not self.transform_inputs: - input_transform = None - - if isinstance(self.transform_inputs, dict): - raise AttributeError( - "Cannot specify dict for transform_inputs when using BatchedModelConstructor" - ) - - if input_transform is None: - botorch.settings.validate_input_scaling(False) - - return input_transform - - def build_model( - self, - input_names: List[str], - outcome_names: List[str], - data: pd.DataFrame, - input_bounds: Dict[str, List] = None, - dtype: torch.dtype = torch.double, - device: Union[torch.device, str] = "cpu", - ) -> SingleTaskGP: - """ - Construct a single batched model for all objectives and constraints. - """ - tkwargs = {"dtype": dtype, "device": device} - - if self.use_cached_hyperparameters: - if self._hyperparameter_store is None: - raise RuntimeWarning( - "cannot use cached hyperparameters, hyperparameter store empty, " - "training GP model hyperparameters instead" - ) - - train_X, train_Y, train_Yvar = get_training_data_batched( - input_names, outcome_names, data - ) - if train_X.shape[0] == 0 or train_Y.shape[0] == 0: - raise ValueError("no data found to train model!") - - # train_Y is n x m, will get transformed to (m) x n x 1 by - # SingleTaskGP to run as m independent batches - # _input_batch_shape = empty - # _aug_batch_shape = (m,) = (_num_outputs,) - - _num_outputs = train_Y.shape[-1] - _input_batch_shape, _aug_batch_shape = ( - BatchedMultiOutputGPyTorchModel.get_batch_dimensions( - train_X=train_X, train_Y=train_Y - ) - ) - # input and output transforms are applied BEFORE tensors are unrolled - input_transform = self._get_input_transform( - outcome_names, - input_names, - input_bounds=input_bounds, - batch_shape=_input_batch_shape, - ) - outcome_transform = Standardize( - m=train_Y.shape[-1], batch_shape=_input_batch_shape - ) - kwargs = { - "input_transform": input_transform, - "outcome_transform": outcome_transform, - } - - if train_Yvar is None: - likelihood = self.get_likelihood(batch_shape=_aug_batch_shape) - else: - likelihood = FixedNoiseGaussianLikelihood( - noise=train_Yvar, batch_shape=_aug_batch_shape - ) - full_model = SingleTaskGP( - train_X, train_Y, train_Yvar=train_Yvar, likelihood=likelihood, **kwargs - ) - full_model.to(**tkwargs) - - if self.use_cached_hyperparameters and self._hyperparameter_store is not None: - store = { - name: ele.to(**tkwargs) - for name, ele in self._hyperparameter_store.items() - } - full_model.load_state_dict(store) - - if self.train_model: - full_model = self._train_model(full_model) - - # cache model hyperparameters - self._hyperparameter_store = full_model.state_dict() - - return full_model diff --git a/xopt/tests/generators/bayesian/test_model_constructor.py b/xopt/tests/generators/bayesian/test_model_constructor.py index 3ace0949d..94cb504a9 100644 --- a/xopt/tests/generators/bayesian/test_model_constructor.py +++ b/xopt/tests/generators/bayesian/test_model_constructor.py @@ -34,7 +34,6 @@ LBFGSNumericalOptimizerConfig, StandardModelConstructor, ) -from xopt.generators.bayesian.models import SaasModelConstructor from xopt.generators.bayesian.utils import get_training_data_batched from xopt.resources.testing import ( TEST_VOCS_BASE, @@ -1271,6 +1270,6 @@ def test_saas_model_constructor(self): test_vocs = deepcopy(TEST_VOCS) test_data = deepcopy(TEST_DATA) - constructor = SaasModelConstructor(warmup_steps=2, num_samples=4, thinning=2) + constructor = StandardModelConstructor(saas_outputs=["y1", "c1"]) model = constructor.build_model_from_vocs(test_vocs, test_data) assert isinstance(model, ModelListGP) From 969ab5d5332ee6ef675183460d5dcd78f6b1a721 Mon Sep 17 00:00:00 2001 From: Ryan Roussel Date: Thu, 16 Apr 2026 15:02:24 -0500 Subject: [PATCH 04/16] updates to example etc --- docs/examples/gp_model_creation/saas.ipynb | 413 ++------------------- xopt/generators/bayesian/base_model.py | 7 +- 2 files changed, 30 insertions(+), 390 deletions(-) diff --git a/docs/examples/gp_model_creation/saas.ipynb b/docs/examples/gp_model_creation/saas.ipynb index fd50beea3..72986e90b 100644 --- a/docs/examples/gp_model_creation/saas.ipynb +++ b/docs/examples/gp_model_creation/saas.ipynb @@ -19,210 +19,9 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\Ryan Roussel\\Documents\\GitHub\\Xopt\\xopt\\pydantic.py:39: UserWarning: Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater.\n", - " from pydantic.v1.json import custom_pydantic_encoder\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
x0x1x2yxopt_runtimexopt_error
01.441835-1.899973-1.3221605.6887873.499968e-06False
10.038882-0.062271-0.0032260.0053901.900014e-06False
2-0.032775-0.712506-1.8203040.5087391.200009e-06False
3-1.714610-0.6104391.0374053.3125239.000069e-07False
4-1.606494-0.620783-1.8383862.9661941.000008e-06False
5-0.553693-1.6406811.7094422.9984109.000069e-07False
6-0.6903900.726443-1.9609291.0043599.000069e-07False
70.067712-0.836368-0.7161850.7040977.000053e-07False
8-1.9846100.3429961.9600664.0563258.000061e-07False
91.861432-0.0700511.4609653.4698377.000053e-07False
101.847126-0.1115691.8200803.4243238.000061e-07False
11-0.2557350.162920-0.5875930.0919431.000008e-06False
12-0.0047481.217890-0.0496321.4832809.000069e-07False
13-1.3940590.2848501.3991672.0245409.000069e-07False
140.8296231.3067971.7292352.3959948.000061e-07False
\n", - "
" - ], - "text/plain": [ - " x0 x1 x2 y xopt_runtime xopt_error\n", - "0 1.441835 -1.899973 -1.322160 5.688787 3.499968e-06 False\n", - "1 0.038882 -0.062271 -0.003226 0.005390 1.900014e-06 False\n", - "2 -0.032775 -0.712506 -1.820304 0.508739 1.200009e-06 False\n", - "3 -1.714610 -0.610439 1.037405 3.312523 9.000069e-07 False\n", - "4 -1.606494 -0.620783 -1.838386 2.966194 1.000008e-06 False\n", - "5 -0.553693 -1.640681 1.709442 2.998410 9.000069e-07 False\n", - "6 -0.690390 0.726443 -1.960929 1.004359 9.000069e-07 False\n", - "7 0.067712 -0.836368 -0.716185 0.704097 7.000053e-07 False\n", - "8 -1.984610 0.342996 1.960066 4.056325 8.000061e-07 False\n", - "9 1.861432 -0.070051 1.460965 3.469837 7.000053e-07 False\n", - "10 1.847126 -0.111569 1.820080 3.424323 8.000061e-07 False\n", - "11 -0.255735 0.162920 -0.587593 0.091943 1.000008e-06 False\n", - "12 -0.004748 1.217890 -0.049632 1.483280 9.000069e-07 False\n", - "13 -1.394059 0.284850 1.399167 2.024540 9.000069e-07 False\n", - "14 0.829623 1.306797 1.729235 2.395994 8.000061e-07 False" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# set values if testing\n", "import os\n", @@ -240,80 +39,21 @@ "\n", "# make rosenbrock function vocs in 3D\n", "vocs = make_rosenbrock_vocs(3)\n", + "vocs.observables = [\"z\"]\n", + "\n", "\n", "def evaluate_func(X):\n", - " return {\"y\": X[\"x0\"] ** 2 + X[\"x1\"] ** 2}\n", + " return {\n", + " \"y\": X[\"x0\"] ** 2 + X[\"x1\"] ** 2,\n", + " \"z\": X[\"x0\"] ** 2,\n", + " }\n", + "\n", "\n", "# collect some data using random sampling\n", "evaluator = Evaluator(function=evaluate_func)\n", "generator = RandomGenerator(vocs=vocs)\n", "X = Xopt(generator=generator, evaluator=evaluator)\n", - "X.random_evaluate(15)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Standard Model Constructor" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "A class for constructing independent models for each objective and constraint.\n", - "\n", - "Attributes\n", - "----------\n", - "name : str\n", - " The name of the model (frozen).\n", - "\n", - "use_low_noise_prior : bool\n", - " Specify if the model should assume a low noise environment.\n", - "\n", - "covar_modules : Dict[str, Kernel]\n", - " Covariance modules for GP models.\n", - "\n", - "mean_modules : Dict[str, Module]\n", - " Prior mean modules for GP models.\n", - "\n", - "trainable_mean_keys : List[str]\n", - " List of prior mean modules that can be trained.\n", - "\n", - "transform_inputs : Union[Dict[str, bool], bool]\n", - " Specify if inputs should be transformed inside the GP model. Can optionally\n", - " specify a dict of specifications.\n", - "\n", - "custom_noise_prior : Optional[Prior]\n", - " Specify a custom noise prior for the GP likelihood. Overwrites value specified\n", - " by use_low_noise_prior.\n", - "\n", - "use_cached_hyperparameters : Optional[bool]\n", - " Flag to specify if cached hyperparameters should be used in model creation.\n", - " Training will still occur unless train_model is False.\n", - "\n", - "train_method : Literal[\"lbfgs\", \"adam\"]\n", - " Numerical optimization algorithm to use.\n", - "\n", - "train_model : bool\n", - " Flag to specify if the model should be trained (fitted to data).\n", - "\n", - "train_config : NumericalOptimizerConfig\n", - " Configuration of the numerical optimizer.\n", - "\n", - "\n" - ] - } - ], - "source": [ - "print(StandardModelConstructor.__doc__)" + "X.random_evaluate(10)" ] }, { @@ -330,7 +70,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -343,38 +83,21 @@ "metadata": {}, "outputs": [], "source": [ - "model_constructor = StandardModelConstructor(saas_outputs=[\"y\"])\n", + "model_constructor = StandardModelConstructor(saas_outputs=[\"z\", \"y\"])\n", "\n", "# here we build a model from info (more flexible)\n", "model = model_constructor.build_model(\n", - " input_names=vocs.variable_names, \n", - " outcome_names=[\"y\"], \n", - " data=data, \n", - ")\n" + " input_names=vocs.variable_names,\n", + " outcome_names=[\"y\", \"z\"],\n", + " data=data,\n", + ")" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "likelihood.noise_covar.raw_noise Parameter containing:\n", - "tensor([-9.2103], requires_grad=True)\n", - "mean_module.raw_constant Parameter containing:\n", - "tensor(0.0375, requires_grad=True)\n", - "covar_module.raw_outputscale Parameter containing:\n", - "tensor(7.7638, requires_grad=True)\n", - "covar_module.base_kernel.raw_lengthscale Parameter containing:\n", - "tensor([[1.7046, 1.8655, 9.2103]], requires_grad=True)\n", - "covar_module.base_kernel.raw_tau Parameter containing:\n", - "tensor(-6.9078, requires_grad=True)\n" - ] - } - ], + "outputs": [], "source": [ "objective_model = model.models[vocs.output_names.index(\"y\")]\n", "\n", @@ -397,109 +120,27 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "findfont: Font family ['DejaVu Sans Display'] not found. Falling back to DejaVu Sans.\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxUAAAFvCAYAAAA8O+aEAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAArhBJREFUeJztnQecU1X2x0/K9IGhD70jRZAOUhQQFBVd2/oHFwUV0WXBXlZcFduKXRQVRAUbLIhdVJQqCkhHaaLS69Db1JT3/5yLCSknmZvJTCbl9/XzHHLffSUvybnvvHt+55gMwzAIAAAAAAAAAEqIuaQbAgAAAAAAAACcCgAAAAAAAEDYYKYCAAAAAAAAEBZwKgAAAAAAAABhAacCAAAAAAAAEBZwKgAAAAAAAABhAacCAAAAAAAAEBZwKgAAAAAAAABhAacCAAAAAAAAEBZwKgAACccff/xBF110EWVlZZHJZKLPP/+8vE8prrjxxhupYcOG5X0aAAAAIgicCgBA1PLuu++qm37XYrVaqU6dOuqmdc+ePSXe79ChQ2ndunX03//+lz744APq1KlTqZ43KDnffPMNPfbYYxG5hLt27aLHH3+cunTpQpUrV6Zq1apR7969ae7cuWHtd9++ffTggw9Snz59qEKFCuq7u3DhQu3t2SHz/N57Ls2aNQvr3AAAoKywltmeAQCglHjiiSeoUaNGVFBQQD///LNyNn766Sdav349paamhrSv/Px8Wrp0Kf3nP/+hUaNG4TMqA9566y1yOp0ldipef/31iDgWX3zxBT377LN05ZVXKkfTbrfT+++/TxdeeCFNnjyZbrrpphLtd/PmzWq/7AC0adNGfd9CYdy4cXTq1Cmvth07dtDDDz+sZtgAACAagVMBAIh6LrnkEvdswi233KKeKPNN25dffkn/93//F9K+Dh48qP5WqlSp1M6PnZ3k5GQymzH5yyQlJVEswDMJO3fuVN8nF//85z+pXbt29Oijj5bYqejYsSMdPnyYqlSpQh9//DFde+21IW3PTo4vTz31lPo7ePDgEp0TAACUNRgBAQAxx3nnnaf+btmyxav9t99+o7///e/qZo5nMNgRYcfDBT/9btCggfr3/fffr8JJPGP/OaTq5ptvpuzsbEpJSaGzzz5bPbH2hMNYeLvp06erJ8ccjpWenk4nTpxQ65ctW0YXX3yx0mtwe69evWjx4sVe++Dz4H38+eefKpSLHRzuzzexeXl5fu/3ww8/VCE6vD8O0zn//PPp+++/9+rz7bffquuSkZGhQm4GDBhAGzZs0A4xW7RoEd12221UtWpVqlixIg0ZMoSOHj3q1/+NN95Q14WvT+3atWnkyJF07NixoJqK7du3q2O88MILNGnSJGrSpInavnPnzrRixQqv7XiWgvEM+fEMK+LP2GazBX1PY8aMUQ7evHnzvNpvvfVW5fz98ssv6jW/D0+HguHzuvTSS2n37t108uRJdzsfk4/N51AcfP35O1iaTJs2Tc3Wde/evVT3CwAApQVmKgAAMQffpDJ8g+2Cb6B79OihbvI5np1vrj/66CP11PeTTz6hq666iq6++mp1A3/33XfTddddp24eMzMz1fY5OTl07rnnqptYDouqXr26ulEfNmyYchjuuusur3N48skn1Q3qfffdR4WFherf8+fPV7Mq/KTadWM7ZcoUuuCCC+jHH39UjoEnPMvCN4pjx46l1atX09tvv001atRQszAuOOafnRC+meQwMD4OOy58LFcoDOtCOHynf//+alt2TCZMmEA9e/akNWvWaImm+T3zteFjcfgOb88hNy4niuF1fD79+vWjESNGuPuxY8COU3EzFHxjzDfq7LzwPp977jn1mWzdulVty+179+6lOXPmqPfky+jRo+m9996jbdu2BX1P7Ox99dVX6rNj7Qzf5H/33XcqLIs/t7Zt2wY9z/379ysHjhdPh7Nly5bqOrMjFkn4M9y0aZMK2QMAgKjFAACAKGXKlCkGm6m5c+caBw8eNHbt2mV8/PHHRvXq1Y2UlBT12kXfvn2NNm3aGAUFBe42p9NpdO/e3WjWrJm7bdu2bWqfzz//vNexhg0bZtSqVcs4dOiQV/ugQYOMrKwsIy8vT71esGCB2r5x48buNtex+Dj9+/dX/3bBfRo1amRceOGF7rYxY8aofdx8881ex7rqqquMqlWrul//8ccfhtlsVu0Oh8Orr+sYJ0+eNCpVqmQMHz7ca/3+/fvVefu2B7rGHTt2NIqKitztzz33nGr/4osv1OsDBw4YycnJxkUXXeR1Lq+99prqN3nyZHfb0KFDjQYNGvhdc35vR44ccbfzvrn9q6++creNHDlStUnwfnkd76841q1bp873lltuMY4ePWrUqVPH6NSpk2Gz2YJux9c8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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "fig, ax = visualize_model(\n", - " model, vocs, data, variable_names=[\"x0\", \"x1\"], reference_point=data.iloc[-1][vocs.variable_names].to_dict()\n", + " model,\n", + " vocs,\n", + " data,\n", + " variable_names=[\"x0\", \"x1\"],\n", + " reference_point=data.iloc[-1][vocs.variable_names].to_dict(),\n", ")" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'x0': 0.8296234531171609, 'x1': 1.3067972159405206, 'x2': 1.7292349786123893}" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "data.iloc[-1][vocs.variable_names].to_dict()" ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Using Custom Kernels via covar_modules\n", - "\n", - "The `covar_modules` parameter in `StandardModelConstructor` allows you to specify custom GPyTorch kernels for specific outputs. This is useful when you have domain knowledge about the function structure." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Custom model with Matern kernel:\n", - "Kernel type: ScaleKernel(\n", - " (base_kernel): MaternKernel(\n", - " (raw_lengthscale_constraint): Positive()\n", - " )\n", - " (raw_outputscale_constraint): Positive()\n", - ")\n", - "Nu parameter: 2.5\n" - ] - } - ], - "source": [ - "from gpytorch.kernels import MaternKernel, ScaleKernel\n", - "\n", - "# Example 1: Use a specific kernel for the objective\n", - "# Here we use a Matern kernel with nu=2.5 for the objective \"y\"\n", - "custom_covar_modules = {\n", - " \"y\": ScaleKernel(MaternKernel(nu=2.5)) # Matern 5/2 kernel for objective\n", - "}\n", - "\n", - "model_constructor_custom = StandardModelConstructor(\n", - " covar_modules=custom_covar_modules, use_low_noise_prior=True\n", - ")\n", - "\n", - "# Build model with custom kernel\n", - "custom_model = model_constructor_custom.build_model_from_vocs(vocs=vocs, data=data)\n", - "\n", - "print(\"Custom model with Matern kernel:\")\n", - "print(f\"Kernel type: {custom_model.models[0].covar_module}\")\n", - "print(f\"Nu parameter: {custom_model.models[0].covar_module.base_kernel.nu}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/xopt/generators/bayesian/base_model.py b/xopt/generators/bayesian/base_model.py index 6a864cf54..4ea4f9235 100644 --- a/xopt/generators/bayesian/base_model.py +++ b/xopt/generators/bayesian/base_model.py @@ -5,8 +5,8 @@ from botorch.exceptions import ModelFittingError import pandas as pd import torch -from botorch import fit_fully_bayesian_model_nuts, fit_gpytorch_mll -from botorch.models import ModelListGP, SaasFullyBayesianSingleTaskGP, SingleTaskGP, SingleTaskVariationalGP +from botorch import fit_gpytorch_mll +from botorch.models import ModelListGP, SingleTaskGP, SingleTaskVariationalGP from botorch.models.model import Model from gpytorch import ExactMarginalLogLikelihood from gpytorch.mlls import VariationalELBO @@ -216,7 +216,6 @@ def build_heteroskedastic_gp( return model @staticmethod - def build_approximate_gp( X: Tensor, Y: Tensor, train: bool = True, **kwargs ) -> Model: @@ -249,7 +248,7 @@ def build_approximate_gp( fit_gpytorch_mll(mll) return model - + @staticmethod def build_map_saas_gp( X: Tensor, Y: Tensor, Yvar: Tensor, train: bool = True, **kwargs From 3661808544dc48ecff3c344c693533631bf5df4f Mon Sep 17 00:00:00 2001 From: Ryan Roussel Date: Thu, 16 Apr 2026 16:09:50 -0500 Subject: [PATCH 05/16] Update saas.ipynb --- docs/examples/gp_model_creation/saas.ipynb | 185 +++++++++++++++++---- 1 file changed, 152 insertions(+), 33 deletions(-) diff --git a/docs/examples/gp_model_creation/saas.ipynb b/docs/examples/gp_model_creation/saas.ipynb index 72986e90b..01f548051 100644 --- a/docs/examples/gp_model_creation/saas.ipynb +++ b/docs/examples/gp_model_creation/saas.ipynb @@ -9,12 +9,12 @@ } }, "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", + "## Demonstrating SAAS Functionality in Xopt GP Models\n", + "This example compares two models trained on the same sparse problem:\n", + "1. A standard GP model.\n", + "2. A GP model with SAAS priors enabled via `saas_outputs`.\n", "\n", - "For this we use the 3D rosenbrock function test function." + "The synthetic objective depends only on `x0` and `x1`, while the remaining dimensions are irrelevant. This makes it easy to see how SAAS encourages sparsity in learned lengthscales." ] }, { @@ -26,19 +26,21 @@ "# set values if testing\n", "import os\n", "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "\n", "from xopt import Xopt, Evaluator\n", "from xopt.generators import RandomGenerator\n", - "from xopt.resources.test_functions.rosenbrock import (\n", - " make_rosenbrock_vocs,\n", - ")\n", - "\n", - "from xopt.generators.bayesian.visualize import visualize_model\n", + "from xopt.resources.test_functions.rosenbrock import make_rosenbrock_vocs\n", "from xopt.generators.bayesian.models import StandardModelConstructor\n", + "from xopt.generators.bayesian.visualize import visualize_model\n", "\n", "SMOKE_TEST = os.environ.get(\"SMOKE_TEST\")\n", "\n", - "# make rosenbrock function vocs in 3D\n", - "vocs = make_rosenbrock_vocs(3)\n", + "# Use a higher-dimensional input space to make sparsity visible.\n", + "n_dim = 20\n", + "n_initial = 15\n", + "vocs = make_rosenbrock_vocs(n_dim)\n", "vocs.observables = [\"z\"]\n", "\n", "\n", @@ -46,14 +48,7 @@ " return {\n", " \"y\": X[\"x0\"] ** 2 + X[\"x1\"] ** 2,\n", " \"z\": X[\"x0\"] ** 2,\n", - " }\n", - "\n", - "\n", - "# collect some data using random sampling\n", - "evaluator = Evaluator(function=evaluate_func)\n", - "generator = RandomGenerator(vocs=vocs)\n", - "X = Xopt(generator=generator, evaluator=evaluator)\n", - "X.random_evaluate(10)" + " }" ] }, { @@ -65,7 +60,7 @@ } }, "source": [ - "## Create GP model based on the data" + "## Generate Training Data" ] }, { @@ -74,7 +69,21 @@ "metadata": {}, "outputs": [], "source": [ - "data = X.data" + "evaluator = Evaluator(function=evaluate_func)\n", + "generator = RandomGenerator(vocs=vocs)\n", + "X = Xopt(generator=generator, evaluator=evaluator)\n", + "X.random_evaluate(n_initial)\n", + "\n", + "data = X.data\n", + "data[vocs.variable_names + [\"y\", \"z\"]].head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Build Standard and SAAS Models\n", + "Both models are trained on the same data so any difference in behavior comes from the SAAS prior configuration." ] }, { @@ -83,10 +92,17 @@ "metadata": {}, "outputs": [], "source": [ - "model_constructor = StandardModelConstructor(saas_outputs=[\"z\", \"y\"])\n", + "standard_constructor = StandardModelConstructor()\n", + "saas_constructor = StandardModelConstructor(saas_outputs=[\"y\", \"z\"])\n", "\n", - "# here we build a model from info (more flexible)\n", - "model = model_constructor.build_model(\n", + "# Build two models from identical data.\n", + "standard_model = standard_constructor.build_model(\n", + " input_names=vocs.variable_names,\n", + " outcome_names=[\"y\", \"z\"],\n", + " data=data,\n", + ")\n", + "\n", + "saas_model = saas_constructor.build_model(\n", " input_names=vocs.variable_names,\n", " outcome_names=[\"y\", \"z\"],\n", " data=data,\n", @@ -99,11 +115,25 @@ "metadata": {}, "outputs": [], "source": [ - "objective_model = model.models[vocs.output_names.index(\"y\")]\n", + "def get_lengthscale_vector(gp_model):\n", + " for name, value in gp_model.named_parameters():\n", + " if \"lengthscale\" in name:\n", + " return value.detach().cpu().numpy().ravel()\n", + " raise RuntimeError(\"No lengthscale parameter found\")\n", + "\n", + "\n", + "standard_y_model = standard_model.models[vocs.output_names.index(\"y\")]\n", + "saas_y_model = saas_model.models[vocs.output_names.index(\"y\")]\n", + "\n", + "comparison = pd.DataFrame(\n", + " {\n", + " \"variable\": vocs.variable_names,\n", + " \"standard_lengthscale\": get_lengthscale_vector(standard_y_model),\n", + " \"saas_lengthscale\": get_lengthscale_vector(saas_y_model),\n", + " }\n", + ")\n", "\n", - "# print raw hyperparameter values\n", - "for name, val in objective_model.named_parameters():\n", - " print(name, val)" + "comparison.sort_values(\"saas_lengthscale\")" ] }, { @@ -115,7 +145,10 @@ } }, "source": [ - "## Visualize model predictions" + "## Compare Learned Lengthscales\n", + "Smaller lengthscales correspond to stronger local sensitivity for that variable.\n", + "Because this objective is sparse, SAAS should concentrate sensitivity on `x0` and `x1` \n", + "while the standard model does not until there is a significant amount of data." ] }, { @@ -124,12 +157,69 @@ "metadata": {}, "outputs": [], "source": [ + "plot_data = comparison.set_index(\"variable\")[\n", + " [\"standard_lengthscale\", \"saas_lengthscale\"]\n", + "]\n", + "\n", + "ax = plot_data.plot(kind=\"bar\", figsize=(10, 4), rot=0)\n", + "ax.set_ylabel(\"Lengthscale\")\n", + "ax.set_title(\"Standard GP vs SAAS GP lengthscales for output y\")\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "reference_point = data.iloc[-1][vocs.variable_names].to_dict()\n", + "\n", "fig, ax = visualize_model(\n", - " model,\n", + " standard_model,\n", " vocs,\n", " data,\n", " variable_names=[\"x0\", \"x1\"],\n", - " reference_point=data.iloc[-1][vocs.variable_names].to_dict(),\n", + " reference_point=reference_point,\n", + ")\n", + "fig.suptitle(\"Standard GP\", y=1.02)\n", + "\n", + "fig, ax = visualize_model(\n", + " saas_model,\n", + " vocs,\n", + " data,\n", + " variable_names=[\"x0\", \"x1\"],\n", + " reference_point=reference_point,\n", + ")\n", + "fig.suptitle(\"SAAS GP\", y=1.02)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Add more data and rebuild models to see how lengthscales evolve." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "X.random_evaluate(n_initial * 2)\n", + "\n", + "# rebuild models with more data to see how lengthscales evolve\n", + "standard_model = standard_constructor.build_model(\n", + " input_names=vocs.variable_names,\n", + " outcome_names=[\"y\", \"z\"],\n", + " data=X.data,\n", + ")\n", + "\n", + "saas_model = saas_constructor.build_model(\n", + " input_names=vocs.variable_names,\n", + " outcome_names=[\"y\", \"z\"],\n", + " data=X.data,\n", ")" ] }, @@ -139,7 +229,36 @@ "metadata": {}, "outputs": [], "source": [ - "data.iloc[-1][vocs.variable_names].to_dict()" + "standard_y_model = standard_model.models[vocs.output_names.index(\"y\")]\n", + "saas_y_model = saas_model.models[vocs.output_names.index(\"y\")]\n", + "\n", + "comparison = pd.DataFrame(\n", + " {\n", + " \"variable\": vocs.variable_names,\n", + " \"standard_lengthscale\": get_lengthscale_vector(standard_y_model),\n", + " \"saas_lengthscale\": get_lengthscale_vector(saas_y_model),\n", + " }\n", + ")\n", + "\n", + "comparison.sort_values(\"saas_lengthscale\")\n", + "\n", + "plot_data = comparison.set_index(\"variable\")[\n", + " [\"standard_lengthscale\", \"saas_lengthscale\"]\n", + "]\n", + "\n", + "ax = plot_data.plot(kind=\"bar\", figsize=(10, 4), rot=0)\n", + "ax.set_ylabel(\"Lengthscale\")\n", + "ax.set_title(\"Standard GP vs SAAS GP lengthscales for output y\")\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Takeaway\n", + "SAAS is enabled by setting `saas_outputs` in `StandardModelConstructor`.\n", + "In sparse problems, SAAS generally produces more selective lengthscales and can improve robustness when many dimensions are weakly relevant." ] } ], From eb231052aef37dd21b6c40acac162816a23d8b0b Mon Sep 17 00:00:00 2001 From: Ryan Roussel Date: Thu, 16 Apr 2026 16:23:53 -0500 Subject: [PATCH 06/16] Update saas.ipynb --- docs/examples/gp_model_creation/saas.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/gp_model_creation/saas.ipynb b/docs/examples/gp_model_creation/saas.ipynb index 01f548051..fe4e5cbc8 100644 --- a/docs/examples/gp_model_creation/saas.ipynb +++ b/docs/examples/gp_model_creation/saas.ipynb @@ -46,7 +46,7 @@ "\n", "def evaluate_func(X):\n", " return {\n", - " \"y\": X[\"x0\"] ** 2 + X[\"x1\"] ** 2,\n", + " \"y\": X[\"x0\"] ** 2 + X[\"x1\"] ** 2 + X[\"x10\"] ** 2,\n", " \"z\": X[\"x0\"] ** 2,\n", " }" ] @@ -207,7 +207,7 @@ "metadata": {}, "outputs": [], "source": [ - "X.random_evaluate(n_initial * 2)\n", + "X.random_evaluate(n_initial)\n", "\n", "# rebuild models with more data to see how lengthscales evolve\n", "standard_model = standard_constructor.build_model(\n", From f7dd7fcdf9a56d51e5c3323ca0598509209c3d5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 15:27:11 +0000 Subject: [PATCH 07/16] test: add SAAS coverage tests for model constructor Agent-Logs-Url: https://github.com/xopt-org/Xopt/sessions/833e3bdf-2ead-4cc3-8ff1-eb88f944d4eb Co-authored-by: roussel-ryan <24279776+roussel-ryan@users.noreply.github.com> --- .../bayesian/test_model_constructor.py | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/xopt/tests/generators/bayesian/test_model_constructor.py b/xopt/tests/generators/bayesian/test_model_constructor.py index 94cb504a9..607cc0bbc 100644 --- a/xopt/tests/generators/bayesian/test_model_constructor.py +++ b/xopt/tests/generators/bayesian/test_model_constructor.py @@ -2,6 +2,7 @@ import os import time from copy import deepcopy +from unittest.mock import patch import numpy as np import pandas as pd @@ -832,6 +833,36 @@ def test_build_models_with_training(self): ) assert isinstance(model_approx, SingleTaskVariationalGP) + # Test build_map_saas_gp with training + model_saas = StandardModelConstructor.build_map_saas_gp(X, Y, Yvar, train=True) + assert isinstance(model_saas, SingleTaskGP) + + def test_build_map_saas_gp(self): + X = torch.randn(10, 2) + Y = torch.randn(10, 1) + Yvar = torch.ones(10, 1) * 0.1 + + with pytest.raises(ValueError, match="no data found to train model!"): + StandardModelConstructor.build_map_saas_gp( + torch.empty(0, 2), torch.empty(0, 1), torch.empty(0, 1), train=False + ) + + import xopt.generators.bayesian.base_model as base_model + + original = base_model.fit_gpytorch_mll + base_model.fit_gpytorch_mll = lambda *args, **kwargs: (_ for _ in ()).throw( + ModelFittingError("fitting failed") + ) + try: + with pytest.warns( + UserWarning, + match="Model fitting failed for MAP SAAS GP. Returning untrained model.", + ): + model = StandardModelConstructor.build_map_saas_gp(X, Y, Yvar, train=True) + assert isinstance(model, SingleTaskGP) + finally: + base_model.fit_gpytorch_mll = original + def test_approximate_gp(self): test_vocs = deepcopy(TEST_VOCS) test_data = deepcopy(TEST_DATA) @@ -1270,6 +1301,23 @@ def test_saas_model_constructor(self): test_vocs = deepcopy(TEST_VOCS) test_data = deepcopy(TEST_DATA) - constructor = StandardModelConstructor(saas_outputs=["y1", "c1"]) - model = constructor.build_model_from_vocs(test_vocs, test_data) + constructor = StandardModelConstructor( + saas_outputs=["y1"], + covar_modules={"y1": ScaleKernel(PeriodicKernel())}, + mean_modules={"y1": ConstantMean()}, + ) + with ( + patch.object( + StandardModelConstructor, + "build_map_saas_gp", + wraps=StandardModelConstructor.build_map_saas_gp, + ) as build_map_saas_gp, + pytest.warns(UserWarning) as warning_records, + ): + model = constructor.build_model_from_vocs(test_vocs, test_data) + + warning_messages = [str(record.message) for record in warning_records] + assert any("Covariance module specified for output y1" in msg for msg in warning_messages) + assert any("Mean module specified for output y1" in msg for msg in warning_messages) + assert build_map_saas_gp.call_count == 1 assert isinstance(model, ModelListGP) From ebcd4ff3b506c6b33b1e14a9fe70496ed61864ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 15:28:03 +0000 Subject: [PATCH 08/16] test: refine SAAS coverage tests with clean patching Agent-Logs-Url: https://github.com/xopt-org/Xopt/sessions/833e3bdf-2ead-4cc3-8ff1-eb88f944d4eb Co-authored-by: roussel-ryan <24279776+roussel-ryan@users.noreply.github.com> --- .../generators/bayesian/test_model_constructor.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/xopt/tests/generators/bayesian/test_model_constructor.py b/xopt/tests/generators/bayesian/test_model_constructor.py index 607cc0bbc..9b5f59423 100644 --- a/xopt/tests/generators/bayesian/test_model_constructor.py +++ b/xopt/tests/generators/bayesian/test_model_constructor.py @@ -27,6 +27,7 @@ from xopt.generators.bayesian.custom_botorch.heteroskedastic import ( XoptHeteroskedasticSingleTaskGP, ) +import xopt.generators.bayesian.base_model as base_model from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator from xopt.generators.bayesian.models.approximate import ApproximateModelConstructor from xopt.generators.bayesian.models.standard import ( @@ -847,21 +848,17 @@ def test_build_map_saas_gp(self): torch.empty(0, 2), torch.empty(0, 1), torch.empty(0, 1), train=False ) - import xopt.generators.bayesian.base_model as base_model - - original = base_model.fit_gpytorch_mll - base_model.fit_gpytorch_mll = lambda *args, **kwargs: (_ for _ in ()).throw( - ModelFittingError("fitting failed") - ) - try: + with patch.object( + base_model, + "fit_gpytorch_mll", + side_effect=ModelFittingError("fitting failed"), + ): with pytest.warns( UserWarning, match="Model fitting failed for MAP SAAS GP. Returning untrained model.", ): model = StandardModelConstructor.build_map_saas_gp(X, Y, Yvar, train=True) assert isinstance(model, SingleTaskGP) - finally: - base_model.fit_gpytorch_mll = original def test_approximate_gp(self): test_vocs = deepcopy(TEST_VOCS) From 7fc9a7c475209eb51900bbe7e7000d0c8c516bbf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 15:28:47 +0000 Subject: [PATCH 09/16] test: rename SAAS mock variable for clarity Agent-Logs-Url: https://github.com/xopt-org/Xopt/sessions/833e3bdf-2ead-4cc3-8ff1-eb88f944d4eb Co-authored-by: roussel-ryan <24279776+roussel-ryan@users.noreply.github.com> --- xopt/tests/generators/bayesian/test_model_constructor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xopt/tests/generators/bayesian/test_model_constructor.py b/xopt/tests/generators/bayesian/test_model_constructor.py index 9b5f59423..630228bd8 100644 --- a/xopt/tests/generators/bayesian/test_model_constructor.py +++ b/xopt/tests/generators/bayesian/test_model_constructor.py @@ -1308,7 +1308,7 @@ def test_saas_model_constructor(self): StandardModelConstructor, "build_map_saas_gp", wraps=StandardModelConstructor.build_map_saas_gp, - ) as build_map_saas_gp, + ) as mock_build_map_saas_gp, pytest.warns(UserWarning) as warning_records, ): model = constructor.build_model_from_vocs(test_vocs, test_data) @@ -1316,5 +1316,5 @@ def test_saas_model_constructor(self): warning_messages = [str(record.message) for record in warning_records] assert any("Covariance module specified for output y1" in msg for msg in warning_messages) assert any("Mean module specified for output y1" in msg for msg in warning_messages) - assert build_map_saas_gp.call_count == 1 + assert mock_build_map_saas_gp.call_count == 1 assert isinstance(model, ModelListGP) From 293988feb044dad48199351105dd19df1c18d743 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 15:29:44 +0000 Subject: [PATCH 10/16] test: strengthen SAAS assertions in coverage tests Agent-Logs-Url: https://github.com/xopt-org/Xopt/sessions/833e3bdf-2ead-4cc3-8ff1-eb88f944d4eb Co-authored-by: roussel-ryan <24279776+roussel-ryan@users.noreply.github.com> --- .../generators/bayesian/test_model_constructor.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/xopt/tests/generators/bayesian/test_model_constructor.py b/xopt/tests/generators/bayesian/test_model_constructor.py index 630228bd8..e9b5cee14 100644 --- a/xopt/tests/generators/bayesian/test_model_constructor.py +++ b/xopt/tests/generators/bayesian/test_model_constructor.py @@ -18,7 +18,7 @@ from gpytorch import ExactMarginalLogLikelihood from gpytorch.kernels import PeriodicKernel, PolynomialKernel, ScaleKernel from gpytorch.likelihoods import GaussianLikelihood -from gpytorch.means import ConstantMean +from gpytorch.means import ConstantMean, LinearMean from gpytorch.priors import GammaPrior from pydantic import ValidationError @@ -835,7 +835,13 @@ def test_build_models_with_training(self): assert isinstance(model_approx, SingleTaskVariationalGP) # Test build_map_saas_gp with training - model_saas = StandardModelConstructor.build_map_saas_gp(X, Y, Yvar, train=True) + with patch.object( + base_model, "fit_gpytorch_mll", wraps=base_model.fit_gpytorch_mll + ) as mock_fit_gpytorch_mll: + model_saas = StandardModelConstructor.build_map_saas_gp( + X, Y, Yvar, train=True + ) + assert mock_fit_gpytorch_mll.call_count == 1 assert isinstance(model_saas, SingleTaskGP) def test_build_map_saas_gp(self): @@ -1301,7 +1307,7 @@ def test_saas_model_constructor(self): constructor = StandardModelConstructor( saas_outputs=["y1"], covar_modules={"y1": ScaleKernel(PeriodicKernel())}, - mean_modules={"y1": ConstantMean()}, + mean_modules={"y1": LinearMean(TEST_VOCS.n_variables)}, ) with ( patch.object( @@ -1317,4 +1323,6 @@ def test_saas_model_constructor(self): assert any("Covariance module specified for output y1" in msg for msg in warning_messages) assert any("Mean module specified for output y1" in msg for msg in warning_messages) assert mock_build_map_saas_gp.call_count == 1 + assert not isinstance(model.models[0].covar_module.base_kernel, PeriodicKernel) + assert not isinstance(model.models[0].mean_module, LinearMean) assert isinstance(model, ModelListGP) From e069cf3d858f87913c1cb7a338f3d439e482e787 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 15:30:39 +0000 Subject: [PATCH 11/16] test: make SAAS coverage checks deterministic and exact Agent-Logs-Url: https://github.com/xopt-org/Xopt/sessions/833e3bdf-2ead-4cc3-8ff1-eb88f944d4eb Co-authored-by: roussel-ryan <24279776+roussel-ryan@users.noreply.github.com> --- .../generators/bayesian/test_model_constructor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/xopt/tests/generators/bayesian/test_model_constructor.py b/xopt/tests/generators/bayesian/test_model_constructor.py index e9b5cee14..a322744eb 100644 --- a/xopt/tests/generators/bayesian/test_model_constructor.py +++ b/xopt/tests/generators/bayesian/test_model_constructor.py @@ -845,6 +845,7 @@ def test_build_models_with_training(self): assert isinstance(model_saas, SingleTaskGP) def test_build_map_saas_gp(self): + torch.manual_seed(0) X = torch.randn(10, 2) Y = torch.randn(10, 1) Yvar = torch.ones(10, 1) * 0.1 @@ -1319,9 +1320,12 @@ def test_saas_model_constructor(self): ): model = constructor.build_model_from_vocs(test_vocs, test_data) - warning_messages = [str(record.message) for record in warning_records] - assert any("Covariance module specified for output y1" in msg for msg in warning_messages) - assert any("Mean module specified for output y1" in msg for msg in warning_messages) + warning_messages = {str(record.message) for record in warning_records} + expected_warning_messages = { + "Covariance module specified for output y1 will be overwritten by SAAS model construction.", + "Mean module specified for output y1 will be overwritten by SAAS model construction.", + } + assert expected_warning_messages.issubset(warning_messages) assert mock_build_map_saas_gp.call_count == 1 assert not isinstance(model.models[0].covar_module.base_kernel, PeriodicKernel) assert not isinstance(model.models[0].mean_module, LinearMean) From 61cb2aad5ffc121379874b89f3ae14871f7b4a7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 15:31:22 +0000 Subject: [PATCH 12/16] test: cover multi-output SAAS configuration Agent-Logs-Url: https://github.com/xopt-org/Xopt/sessions/833e3bdf-2ead-4cc3-8ff1-eb88f944d4eb Co-authored-by: roussel-ryan <24279776+roussel-ryan@users.noreply.github.com> --- .../generators/bayesian/test_model_constructor.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/xopt/tests/generators/bayesian/test_model_constructor.py b/xopt/tests/generators/bayesian/test_model_constructor.py index a322744eb..945d2c754 100644 --- a/xopt/tests/generators/bayesian/test_model_constructor.py +++ b/xopt/tests/generators/bayesian/test_model_constructor.py @@ -1330,3 +1330,13 @@ def test_saas_model_constructor(self): assert not isinstance(model.models[0].covar_module.base_kernel, PeriodicKernel) assert not isinstance(model.models[0].mean_module, LinearMean) assert isinstance(model, ModelListGP) + + constructor_multi = StandardModelConstructor(saas_outputs=["y1", "c1"]) + with patch.object( + StandardModelConstructor, + "build_map_saas_gp", + wraps=StandardModelConstructor.build_map_saas_gp, + ) as mock_build_map_saas_gp_multi: + model_multi = constructor_multi.build_model_from_vocs(test_vocs, test_data) + assert mock_build_map_saas_gp_multi.call_count == 2 + assert isinstance(model_multi, ModelListGP) From 38ffa2ff7379c6eb33ff6540cae72377d9fe23df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 15:32:20 +0000 Subject: [PATCH 13/16] test: assert expected SAAS model module types Agent-Logs-Url: https://github.com/xopt-org/Xopt/sessions/833e3bdf-2ead-4cc3-8ff1-eb88f944d4eb Co-authored-by: roussel-ryan <24279776+roussel-ryan@users.noreply.github.com> --- xopt/tests/generators/bayesian/test_model_constructor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/xopt/tests/generators/bayesian/test_model_constructor.py b/xopt/tests/generators/bayesian/test_model_constructor.py index 945d2c754..82d5f0299 100644 --- a/xopt/tests/generators/bayesian/test_model_constructor.py +++ b/xopt/tests/generators/bayesian/test_model_constructor.py @@ -852,7 +852,7 @@ def test_build_map_saas_gp(self): with pytest.raises(ValueError, match="no data found to train model!"): StandardModelConstructor.build_map_saas_gp( - torch.empty(0, 2), torch.empty(0, 1), torch.empty(0, 1), train=False + torch.empty(0, 2), torch.empty(0, 1), torch.empty(0, 1) ) with patch.object( @@ -1327,6 +1327,9 @@ def test_saas_model_constructor(self): } assert expected_warning_messages.issubset(warning_messages) assert mock_build_map_saas_gp.call_count == 1 + assert isinstance(model.models[0], SingleTaskGP) + assert isinstance(model.models[0].covar_module, ScaleKernel) + assert isinstance(model.models[0].mean_module, ConstantMean) assert not isinstance(model.models[0].covar_module.base_kernel, PeriodicKernel) assert not isinstance(model.models[0].mean_module, LinearMean) assert isinstance(model, ModelListGP) From 3944f6a1889852922c9ec113528ab01eecd12776 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 15:33:08 +0000 Subject: [PATCH 14/16] test: tighten SAAS warning matching and remove duplication Agent-Logs-Url: https://github.com/xopt-org/Xopt/sessions/833e3bdf-2ead-4cc3-8ff1-eb88f944d4eb Co-authored-by: roussel-ryan <24279776+roussel-ryan@users.noreply.github.com> --- .../generators/bayesian/test_model_constructor.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/xopt/tests/generators/bayesian/test_model_constructor.py b/xopt/tests/generators/bayesian/test_model_constructor.py index 82d5f0299..b7a0d4eec 100644 --- a/xopt/tests/generators/bayesian/test_model_constructor.py +++ b/xopt/tests/generators/bayesian/test_model_constructor.py @@ -834,16 +834,6 @@ def test_build_models_with_training(self): ) assert isinstance(model_approx, SingleTaskVariationalGP) - # Test build_map_saas_gp with training - with patch.object( - base_model, "fit_gpytorch_mll", wraps=base_model.fit_gpytorch_mll - ) as mock_fit_gpytorch_mll: - model_saas = StandardModelConstructor.build_map_saas_gp( - X, Y, Yvar, train=True - ) - assert mock_fit_gpytorch_mll.call_count == 1 - assert isinstance(model_saas, SingleTaskGP) - def test_build_map_saas_gp(self): torch.manual_seed(0) X = torch.randn(10, 2) @@ -1316,7 +1306,9 @@ def test_saas_model_constructor(self): "build_map_saas_gp", wraps=StandardModelConstructor.build_map_saas_gp, ) as mock_build_map_saas_gp, - pytest.warns(UserWarning) as warning_records, + pytest.warns( + UserWarning, match="will be overwritten by SAAS model construction" + ) as warning_records, ): model = constructor.build_model_from_vocs(test_vocs, test_data) From 7b6b43d875221db6ec028d6dfaf09f23288cb57e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 15:34:00 +0000 Subject: [PATCH 15/16] test: clarify SAAS patch target and kernel assertions Agent-Logs-Url: https://github.com/xopt-org/Xopt/sessions/833e3bdf-2ead-4cc3-8ff1-eb88f944d4eb Co-authored-by: roussel-ryan <24279776+roussel-ryan@users.noreply.github.com> --- xopt/tests/generators/bayesian/test_model_constructor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/xopt/tests/generators/bayesian/test_model_constructor.py b/xopt/tests/generators/bayesian/test_model_constructor.py index b7a0d4eec..3a735037a 100644 --- a/xopt/tests/generators/bayesian/test_model_constructor.py +++ b/xopt/tests/generators/bayesian/test_model_constructor.py @@ -27,7 +27,6 @@ from xopt.generators.bayesian.custom_botorch.heteroskedastic import ( XoptHeteroskedasticSingleTaskGP, ) -import xopt.generators.bayesian.base_model as base_model from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator from xopt.generators.bayesian.models.approximate import ApproximateModelConstructor from xopt.generators.bayesian.models.standard import ( @@ -845,9 +844,8 @@ def test_build_map_saas_gp(self): torch.empty(0, 2), torch.empty(0, 1), torch.empty(0, 1) ) - with patch.object( - base_model, - "fit_gpytorch_mll", + with patch( + "xopt.generators.bayesian.base_model.fit_gpytorch_mll", side_effect=ModelFittingError("fitting failed"), ): with pytest.warns( @@ -1322,6 +1320,7 @@ def test_saas_model_constructor(self): assert isinstance(model.models[0], SingleTaskGP) assert isinstance(model.models[0].covar_module, ScaleKernel) assert isinstance(model.models[0].mean_module, ConstantMean) + assert hasattr(model.models[0].covar_module.base_kernel, "lengthscale") assert not isinstance(model.models[0].covar_module.base_kernel, PeriodicKernel) assert not isinstance(model.models[0].mean_module, LinearMean) assert isinstance(model, ModelListGP) From f398fb818d0700a904d41ee034f264aec72d7433 Mon Sep 17 00:00:00 2001 From: Ryan Roussel Date: Fri, 15 May 2026 21:56:56 -0500 Subject: [PATCH 16/16] Update test_model_constructor.py --- xopt/tests/generators/bayesian/test_model_constructor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/xopt/tests/generators/bayesian/test_model_constructor.py b/xopt/tests/generators/bayesian/test_model_constructor.py index 3a735037a..ddd04a574 100644 --- a/xopt/tests/generators/bayesian/test_model_constructor.py +++ b/xopt/tests/generators/bayesian/test_model_constructor.py @@ -852,7 +852,9 @@ def test_build_map_saas_gp(self): UserWarning, match="Model fitting failed for MAP SAAS GP. Returning untrained model.", ): - model = StandardModelConstructor.build_map_saas_gp(X, Y, Yvar, train=True) + model = StandardModelConstructor.build_map_saas_gp( + X, Y, Yvar, train=True + ) assert isinstance(model, SingleTaskGP) def test_approximate_gp(self):