Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
d08a695
Create contextual_ei.py
roussel-ryan Jun 5, 2025
868a810
Merge branch 'main' into contextual_bo_generator
roussel-ryan Dec 11, 2025
93ab5d3
updates to fix bugs in contextual EI
roussel-ryan Dec 11, 2025
f794c46
Merge branch 'main' into contextual_bo_generator
roussel-ryan Jun 1, 2026
110fac6
fixes to pass EI tests
roussel-ryan Jun 1, 2026
1099c4c
generalize implementation
roussel-ryan Jun 1, 2026
3b522cb
add fixes and update example
roussel-ryan Jun 1, 2026
ed97f07
linting
roussel-ryan Jun 1, 2026
787a1d6
fix visualization when context variables are present
roussel-ryan Jun 1, 2026
0eb2281
Update visualize.py
roussel-ryan Jun 2, 2026
a674367
Fix convert_numpy_to_inputs variable ordering, fix get_optimum with c…
Copilot Jun 2, 2026
edfa123
Fix 3.14 -> np.pi in test and improve convert_numpy_to_inputs docstring
Copilot Jun 2, 2026
a3939e4
linting
roussel-ryan Jun 3, 2026
a7ee1c7
fix for botorch updates
roussel-ryan Jun 3, 2026
53175a7
more bugfixes for recent botorch updates
roussel-ryan Jun 3, 2026
5063298
Initial plan
Copilot Jun 3, 2026
ff26974
Fix contextual BO: candidate names, bounds indexing, and error message
Copilot Jun 3, 2026
a244e7a
Add tests for contextual BO with fixed features
Copilot Jun 3, 2026
22002d5
Merge pull request #426 from xopt-org/copilot/fix-comments-in-review-…
roussel-ryan Jun 3, 2026
3201e8b
linting
roussel-ryan Jun 3, 2026
ff3ee27
Merge branch 'main' into contextual_bo_generator
roussel-ryan Jun 8, 2026
94b5bcc
change base class of ContextualVariable
roussel-ryan Jun 8, 2026
77916e7
remove unnecessary test
roussel-ryan Jun 8, 2026
bebdc18
linting
roussel-ryan Jun 8, 2026
4082776
Delete uv.lock
roussel-ryan Jun 10, 2026
8d5dd44
Merge branch 'main' into contextual_bo_generator
roussel-ryan Jun 10, 2026
3b7f691
fix merge issues
roussel-ryan Jun 10, 2026
44d23f1
linting
roussel-ryan Jun 10, 2026
f8d0d1d
updates to have contextual variable inherit from Continuous variable
roussel-ryan Jun 29, 2026
6ced6a8
docstring update
roussel-ryan Jun 29, 2026
150e1e6
add supports contextual variables flag
roussel-ryan Jun 29, 2026
c5294c7
Merge branch 'main' into contextual_bo_generator
roussel-ryan Jun 29, 2026
bbccce5
add docstring
roussel-ryan Jun 29, 2026
4b0cfa8
fix turbo test
roussel-ryan Jun 29, 2026
7286c95
Merge branch 'main' into contextual_bo_generator
roussel-ryan Jul 8, 2026
b25a6ca
add tests for coverage
roussel-ryan Jul 8, 2026
30153ff
linting
roussel-ryan Jul 8, 2026
77b6e6b
Merge branch 'main' into contextual_bo_generator
roussel-ryan Jul 9, 2026
1d385f2
fix plotting issues
roussel-ryan Jul 9, 2026
3d0b09d
Merge branch 'main' into contextual_bo_generator
roussel-ryan Jul 10, 2026
3c5cd1a
add tests for vocs to reach 100% coverage
roussel-ryan Jul 10, 2026
a56b5c4
linting
roussel-ryan Jul 10, 2026
c9c36a1
update to use ContextualVariable from gest-api
roussel-ryan Jul 17, 2026
293e43a
modify serialization to handle inf, test roundtrip serialization for …
roussel-ryan Jul 17, 2026
894cbc3
Update pyproject.toml
roussel-ryan Jul 17, 2026
d51e6c8
libensemble pinning
roussel-ryan Jul 17, 2026
d149432
make aposmm tests xfail until libensemble supports gest-api v0.2
roussel-ryan Jul 17, 2026
c8b8ef9
revert libensemble requirement
roussel-ryan Jul 17, 2026
9602a02
add cov ignore for xfail tests
roussel-ryan Jul 17, 2026
3dbf210
reduce if branches to max coverage
roussel-ryan Jul 17, 2026
df23861
linting
roussel-ryan Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
380 changes: 380 additions & 0 deletions docs/examples/single_objective_bayes_opt/contextual_bo.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,380 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "26334b35",
"metadata": {},
"source": [
"# Contextual Bayesian Optimization\n",
"This example compares standard Bayesian optimization against contextual Bayesian optimization on a drifting objective.\n",
"\n",
"The optimization variable is `x1`, while `context` is observed after each evaluation but cannot be controlled. In the contextual setup, define `context` as a `ContextualVariable` in `VOCS`.\n",
"\n",
"> `ContextualVariable()` defaults to an unbounded domain `(-inf, inf)`. When finite bounds are needed (for example, model input bounds or visualization meshes), Xopt infers them from current data. If you provide an explicit finite domain, that explicit domain takes precedence over inferred bounds.\n",
"\n",
"The Bayesian generator conditions on the latest observed context value when optimizing the acquisition function."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d711b0e6",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T19:58:42.324890Z",
"iopub.status.busy": "2026-06-01T19:58:42.324706Z",
"iopub.status.idle": "2026-06-01T19:58:45.435049Z",
"shell.execute_reply": "2026-06-01T19:58:45.434387Z"
}
},
"outputs": [],
"source": [
"import os\n",
"import warnings\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"from xopt import Evaluator\n",
"from xopt.vocs import VOCS, ContextualVariable\n",
"\n",
"warnings.filterwarnings(\n",
" \"ignore\", message=\"Data \\(outcome observations\\) is not standardized.*\"\n",
")\n",
"\n",
"SMOKE_TEST = os.environ.get(\"SMOKE_TEST\", \"\").lower() in [\"1\", \"true\", \"yes\"]\n",
"N_INITIAL = 3 if SMOKE_TEST else 8\n",
"N_STEPS = 8 if SMOKE_TEST else 50"
]
},
{
"cell_type": "markdown",
"id": "3f117ad0",
"metadata": {},
"source": [
"## Drifting Objective\n",
"We construct a simple non-stationary objective where the optimum moves over time. The evaluator reports both the objective value `y` and the observed context value `context` at each iteration."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f1416bd3",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T19:58:45.437428Z",
"iopub.status.busy": "2026-06-01T19:58:45.437117Z",
"iopub.status.idle": "2026-06-01T19:58:45.441415Z",
"shell.execute_reply": "2026-06-01T19:58:45.440827Z"
}
},
"outputs": [],
"source": [
"class DriftingObjective:\n",
" def __init__(self, period: int = 20):\n",
" self.period = period\n",
" self.iteration = 0\n",
"\n",
" def __call__(self, inputs):\n",
" x = float(inputs[\"x1\"])\n",
" context = 0.5 * (1.0 + np.sin(2.0 * np.pi * self.iteration / self.period))\n",
"\n",
" # Peak value is 1.0 when x1 equals the current context.\n",
" y = -((x - context) ** 2)\n",
"\n",
" self.iteration += 1\n",
" return {\n",
" \"y\": float(y),\n",
" \"context\": float(context),\n",
" \"tracking_error\": float(abs(x - context)),\n",
" }"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f74d152b",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T19:58:45.442900Z",
"iopub.status.busy": "2026-06-01T19:58:45.442733Z",
"iopub.status.idle": "2026-06-01T19:58:45.447191Z",
"shell.execute_reply": "2026-06-01T19:58:45.446612Z"
}
},
"outputs": [],
"source": [
"from xopt.base import Xopt\n",
"from xopt.generators.bayesian.upper_confidence_bound import (\n",
" UpperConfidenceBoundGenerator,\n",
")\n",
"\n",
"\n",
"def run_optimization(use_context: bool):\n",
" objective = DriftingObjective(25)\n",
" evaluator = Evaluator(function=objective)\n",
"\n",
" if use_context:\n",
" vocs = VOCS(\n",
" # Default ContextualVariable() uses unbounded domain and infers finite\n",
" # bounds from observed data when needed. To force fixed bounds, use:\n",
" # ContextualVariable(domain=[low, high]).\n",
" variables={\"x1\": [0.0, 1.0], \"context\": ContextualVariable()},\n",
" objectives={\"y\": \"MAXIMIZE\"},\n",
" observables=[\"context\", \"tracking_error\"],\n",
" )\n",
" else:\n",
" vocs = VOCS(\n",
" variables={\"x1\": [0.0, 1.0]},\n",
" objectives={\"y\": \"MAXIMIZE\"},\n",
" observables=[\"context\", \"tracking_error\"],\n",
" )\n",
"\n",
" generator = UpperConfidenceBoundGenerator(vocs=vocs)\n",
" generator.gp_constructor.use_low_noise_prior = True\n",
"\n",
" X = Xopt(generator=generator, evaluator=evaluator)\n",
"\n",
" # Build initial data manually since contextual variables are observed, not sampled.\n",
" initial_inputs = pd.DataFrame({\"x1\": np.linspace(0.05, 0.95, N_INITIAL)})\n",
" X.evaluate_data(initial_inputs)\n",
"\n",
" for _ in range(N_STEPS):\n",
" X.step()\n",
"\n",
" data = X.data\n",
" data[\"iteration\"] = np.arange(len(data))\n",
"\n",
" return X.generator, data"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "149e9351",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T19:58:45.448624Z",
"iopub.status.busy": "2026-06-01T19:58:45.448461Z",
"iopub.status.idle": "2026-06-01T19:58:54.598399Z",
"shell.execute_reply": "2026-06-01T19:58:54.597607Z"
}
},
"outputs": [],
"source": [
"np.random.seed(1)\n",
"\n",
"plain_generator, plain_data = run_optimization(use_context=False)\n",
"contextual_generator, contextual_data = run_optimization(use_context=True)\n",
"\n",
"print(f\"Standard BO evaluations: {len(plain_data)}\")\n",
"print(f\"Contextual BO evaluations: {len(contextual_data)}\")"
]
},
{
"cell_type": "markdown",
"id": "f6438da2",
"metadata": {},
"source": [
"## Compare Standard BO and Contextual BO\n",
"In the first panel, we compare selected `x1` values against the drifting context. In the second panel, we compare rolling objective values. In the third panel, we compare rolling tracking error (lower is better)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "69ab860b",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T19:58:54.600257Z",
"iopub.status.busy": "2026-06-01T19:58:54.600011Z",
"iopub.status.idle": "2026-06-01T19:58:55.021526Z",
"shell.execute_reply": "2026-06-01T19:58:55.020720Z"
}
},
"outputs": [],
"source": [
"window = 1 if not SMOKE_TEST else 3\n",
"\n",
"fig, axes = plt.subplots(1, 3, figsize=(16, 4))\n",
"\n",
"axes[0].plot(plain_data[\"iteration\"], plain_data[\"x1\"], label=\"standard BO\", lw=1.8)\n",
"axes[0].plot(\n",
" contextual_data[\"iteration\"],\n",
" contextual_data[\"x1\"],\n",
" label=\"contextual BO\",\n",
" lw=1.8,\n",
")\n",
"axes[0].plot(\n",
" plain_data[\"iteration\"],\n",
" plain_data[\"context\"],\n",
" \"k--\",\n",
" label=\"observed context\",\n",
" alpha=0.8,\n",
")\n",
"axes[0].set_title(\"Chosen x1 vs Context\")\n",
"axes[0].set_xlabel(\"iteration\")\n",
"axes[0].set_ylabel(\"value\")\n",
"axes[0].legend(loc=\"best\")\n",
"\n",
"axes[1].plot(\n",
" plain_data[\"iteration\"],\n",
" plain_data[\"y\"].rolling(window, min_periods=1).mean(),\n",
" label=\"standard BO\",\n",
")\n",
"axes[1].plot(\n",
" contextual_data[\"iteration\"],\n",
" contextual_data[\"y\"].rolling(window, min_periods=1).mean(),\n",
" label=\"contextual BO\",\n",
")\n",
"axes[1].set_title(f\"Rolling mean objective (window={window})\")\n",
"axes[1].set_xlabel(\"iteration\")\n",
"axes[1].set_ylabel(\"y\")\n",
"axes[1].legend(loc=\"best\")\n",
"\n",
"axes[2].plot(\n",
" plain_data[\"iteration\"],\n",
" plain_data[\"tracking_error\"].rolling(window, min_periods=1).mean(),\n",
" label=\"standard BO\",\n",
")\n",
"axes[2].plot(\n",
" contextual_data[\"iteration\"],\n",
" contextual_data[\"tracking_error\"].rolling(window, min_periods=1).mean(),\n",
" label=\"contextual BO\",\n",
")\n",
"axes[2].set_title(f\"Rolling mean tracking error (window={window})\")\n",
"axes[2].set_xlabel(\"iteration\")\n",
"axes[2].set_ylabel(\"|x1 - context|\")\n",
"axes[2].legend(loc=\"best\")\n",
"\n",
"plt.tight_layout()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "84bb2d01",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T19:58:55.023303Z",
"iopub.status.busy": "2026-06-01T19:58:55.023130Z",
"iopub.status.idle": "2026-06-01T19:58:55.031514Z",
"shell.execute_reply": "2026-06-01T19:58:55.030886Z"
}
},
"outputs": [],
"source": [
"comparison = pd.DataFrame(\n",
" {\n",
" \"mean_y\": [plain_data[\"y\"].mean(), contextual_data[\"y\"].mean()],\n",
" \"mean_tracking_error\": [\n",
" plain_data[\"tracking_error\"].mean(),\n",
" contextual_data[\"tracking_error\"].mean(),\n",
" ],\n",
" },\n",
" index=[\"standard_bo\", \"contextual_bo\"],\n",
")\n",
"\n",
"comparison"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "df537bc9",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-01T19:58:55.033064Z",
"iopub.status.busy": "2026-06-01T19:58:55.032880Z",
"iopub.status.idle": "2026-06-01T19:58:55.036313Z",
"shell.execute_reply": "2026-06-01T19:58:55.035675Z"
}
},
"outputs": [],
"source": [
"if (\n",
" comparison.loc[\"contextual_bo\", \"mean_tracking_error\"]\n",
" < comparison.loc[\"standard_bo\", \"mean_tracking_error\"]\n",
"):\n",
" print(\"Contextual BO tracks the drifting optimum better in this run.\")\n",
"else:\n",
" print(\n",
" \"In this run, contextual BO did not improve tracking error. Try changing the period or number of steps.\"\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "7bc449af",
"metadata": {},
"source": [
"## Visualizing contextual models and acquisition functions\n",
"These plots inspect the surrogate models learned by the contextual and standard generators. For the contextual generator, the model depends on both x1 and the observed context value, so visualization can show either the full model or a slice over selected variables.\n",
"\n",
"Note that we cannot visualize the acquisition function in the 2D plots shown below because it is conditioned on the context value, however we can visualize it in 1D because the context value is given by the reference point."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e346ef02",
"metadata": {},
"outputs": [],
"source": [
"contextual_generator.visualize_model()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a878756e",
"metadata": {},
"outputs": [],
"source": [
"contextual_generator.visualize_model(\n",
" variable_names=[\"x1\"],\n",
" show_samples=False,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4c6a47d8",
"metadata": {},
"outputs": [],
"source": [
"plain_generator.visualize_model()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e6942e0d",
"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.13.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ nav:
- Noisy Bayesian optimization: examples/single_objective_bayes_opt/noisy_bo_tutorial.ipynb
- Fast model evaluation: examples/single_objective_bayes_opt/fast_model_eval.ipynb
- Hessian kernel: examples/single_objective_bayes_opt/hessian_kernel.ipynb
- Contextual Bayesian optimization: examples/single_objective_bayes_opt/contextual_bo.ipynb
- Trust Region Bayesian Optimization:
- Basic example: examples/trust_region_bo/turbo_basics.ipynb
- Optimization: examples/trust_region_bo/turbo_optimize.ipynb
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ dependencies = [
"tqdm",
"orjson",
"matplotlib",
"gest-api"
"gest-api>=0.2"
]
description = "Flexible optimization of arbitrary problems in Python."
dynamic = [ "version" ]
Expand Down
Loading
Loading