Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interpolated BO generators #174

Merged
merged 8 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
452 changes: 452 additions & 0 deletions docs/examples/bayes_exp/bayesian_exploration_w_interpolation.ipynb

Large diffs are not rendered by default.

356 changes: 356 additions & 0 deletions docs/examples/single_objective_bayes_opt/interpolate_tutorial.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ nav:
- Upper confidence bound: examples/single_objective_bayes_opt/upper_confidence_bound.ipynb
- Custom GP models: examples/single_objective_bayes_opt/custom_model.ipynb
- Trust region: examples/single_objective_bayes_opt/turbo_tutorial.ipynb
- Interpolated optimization: examples/single_objective_bayes_opt/interpolate_tutorial.ipynb
- Multi-Fidelity: examples/single_objective_bayes_opt/multi_fidelity_simple.ipynb
- Time dependent upper confidence bound: examples/single_objective_bayes_opt/time_dependent_bo.ipynb
- Bayesian Algorithm Execution: examples/single_objective_bayes_opt/bax_tutorial.ipynb
Expand Down
20 changes: 20 additions & 0 deletions tests/generators/bayesian/test_bayesian_exploration.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,23 @@ def test_in_xopt(self):
# now use bayes opt
X.step()
X.step()

def test_interpolation(self):
evaluator = Evaluator(function=xtest_callable)

test_vocs = deepcopy(TEST_VOCS_BASE)
test_vocs.objectives = {}
test_vocs.observables = ["y1"]

gen = BayesianExplorationGenerator(vocs=test_vocs)
gen.numerical_optimizer.n_restarts = 1
gen.n_monte_carlo_samples = 1
gen.n_interpolate_points = 5

X = Xopt(generator=gen, evaluator=evaluator, vocs=TEST_VOCS_BASE)
X.add_data(TEST_VOCS_DATA)

# now use bayes opt
X.step()
X.step()
assert len(X.data) == 20
7 changes: 3 additions & 4 deletions xopt/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,18 +319,17 @@ def evaluate_data(

if self.strict:
validate_outputs(output_data)
new_data = pd.concat([input_data, output_data], axis=1)

# explode any list like results if all the output names exist
new_data = explode_all_columns(new_data)
output_data = explode_all_columns(output_data)

self.add_data(new_data)
self.add_data(output_data)

# dump data to file if specified
if self.dump_file is not None:
self.dump()

return new_data
return output_data

def add_data(self, new_data: pd.DataFrame):
"""
Expand Down
6 changes: 4 additions & 2 deletions xopt/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ def evaluate_data(
kwargs,
)

return DataFrame(output_data, index=input_data.index)
return pd.concat(
[input_data, DataFrame(output_data, index=input_data.index)], axis=1
)

def safe_function(self, *args, **kwargs):
"""
Expand Down Expand Up @@ -226,7 +228,7 @@ def validate_outputs(outputs: DataFrame):
error_string = "Xopt evaluator caught exception(s):\n\n"
for i in range(len(outputs["xopt_error_str"])):
error_string += f"Evaluation index {i}:\n"
error_string += outputs["xopt_error_str"].iloc[i]
error_string += str(outputs["xopt_error_str"].iloc[i])
error_string += "\n"

raise XoptError(error_string)
Expand Down
30 changes: 28 additions & 2 deletions xopt/generators/bayesian/bayesian_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from botorch.sampling import get_sampler, SobolQMCNormalSampler
from botorch.utils.multi_objective.box_decompositions import DominatedPartitioning
from gpytorch import Module
from pydantic import Field, field_validator, SerializeAsAny
from pydantic import Field, field_validator, PositiveInt, SerializeAsAny
from pydantic_core.core_schema import ValidationInfo
from torch import Tensor

Expand All @@ -36,7 +36,11 @@
SafetyTurboController,
TurboController,
)
from xopt.generators.bayesian.utils import rectilinear_domain_union, set_botorch_weights
from xopt.generators.bayesian.utils import (
interpolate_points,
rectilinear_domain_union,
set_botorch_weights,
)
from xopt.generators.bayesian.visualize import visualize_generator_model
from xopt.numerical_optimizer import GridOptimizer, LBFGSOptimizer, NumericalOptimizer
from xopt.pydantic import decode_torch_module
Expand Down Expand Up @@ -95,6 +99,10 @@ class BayesianGenerator(Generator, ABC):
Flag to determine if final acquisition function value should be
log-transformed before optimization.

n_interpolate_samples: Optional[PositiveInt]
Number of interpolation points to generate between last observation and next
observation, requires n_candidates to be 1.

n_candidates : int
The number of candidates to generate in each optimization step.

Expand Down Expand Up @@ -153,6 +161,7 @@ class BayesianGenerator(Generator, ABC):
False,
description="flag to log transform the acquisition function before optimization",
)
n_interpolate_points: Optional[PositiveInt] = None

n_candidates: int = 1

Expand Down Expand Up @@ -329,6 +338,23 @@ def generate(self, n_candidates: int) -> list[dict]:
else:
self.computation_time = pd.DataFrame(timing_results, index=[0])

if self.n_interpolate_points is not None:
if self.n_candidates > 1:
raise RuntimeError(
"cannot generate interpolated points for "
"multiple candidate generation"
)
else:
assert len(result) == 1
result = interpolate_points(
pd.concat(
(self.data.iloc[-1:][self.vocs.variable_names], result),
axis=0,
ignore_index=True,
),
num_points=self.n_interpolate_points,
)

return result.to_dict("records")

def train_model(self, data: pd.DataFrame = None, update_internal=True) -> Module:
Expand Down
37 changes: 37 additions & 0 deletions xopt/generators/bayesian/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Dict, List

import numpy as np
import pandas as pd
import torch
from botorch.models.transforms import Normalize
Expand Down Expand Up @@ -83,3 +84,39 @@ def rectilinear_domain_union(A, B):
)

return out_bounds


def interpolate_points(df, num_points=10):
"""
Generates interpolated points between two points specified by a pandas DataFrame.

Parameters
----------
df: DataFrame
with two rows representing the start and end points.
num_points: int
Number of points to generate between the start and end points.

Returns
-------
result: DataFrame
DataFrame with the interpolated points.
"""
if df.shape[0] != 2:
raise ValueError("Input DataFrame must have exactly two rows.")

start_point = df.iloc[0]
end_point = df.iloc[1]

# Create an array of num_points equally spaced between 0 and 1
interpolation_factors = np.linspace(0, 1, num_points + 1)

# Interpolate each column independently
interpolated_points = pd.DataFrame()
for col in df.columns:
interpolated_values = np.interp(
interpolation_factors, [0, 1], [start_point[col], end_point[col]]
)
interpolated_points[col] = interpolated_values[1:]

return interpolated_points
Loading