-
Notifications
You must be signed in to change notification settings - Fork 47
Ship PairwiseGP from Botorch into BoFire #768
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
Changes from 7 commits
457ab02
6821f42
af1404c
7e7b003
2aad969
f9a01e1
2f6541e
65b7152
45fb402
73481ac
efa3a6c
45d5278
27a5538
92c648d
bdad654
7d81545
6bcab72
2b381c0
2b11a81
bb4d1fd
5485c0c
b478663
897abd0
e912190
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -162,3 +162,5 @@ notebook_test_stats.csv | |
| **/*.quarto_ipynb | ||
|
|
||
| **/.jupyter_cache | ||
|
|
||
| scripts/* | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| from typing import Literal | ||
|
|
||
| from pydantic import PositiveFloat, model_validator | ||
|
|
||
| from bofire.data_models.priors.prior import Prior | ||
|
|
||
|
|
||
| class SmoothedBoxPrior(Prior): | ||
| """A smoothed approximation of a uniform prior. | ||
|
|
||
| .. math:: | ||
|
|
||
| \begin{equation*} | ||
| B = {x: a_i <= x_i <= b_i} | ||
| d(x, B) = min_{x' in B} |x - x'| | ||
| pdf(x) \\sim exp(- d(x, B)**2 / sqrt(2 * sigma^2)) | ||
| \\end{equation*} | ||
|
|
||
| Attributes: | ||
| lower_bound: lower bound of the uniform prior | ||
| upper_bound: upper bound of the uniform prior | ||
| sigma: related to pdf(x) | ||
|
|
||
| """ | ||
|
|
||
| type: Literal["SmoothedBoxPrior"] = "SmoothedBoxPrior" | ||
| lower_bound: float | ||
| upper_bound: float | ||
| sigma: PositiveFloat = 0.01 | ||
|
|
||
| @model_validator(mode="after") | ||
| def validate_bounds(self): | ||
| if self.lower_bound >= self.upper_bound: | ||
| raise ValueError( | ||
| "The lower bound must be less than the upper bound for an interval." | ||
| ) | ||
| return self |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| from typing import Literal, Type | ||
|
|
||
| from pydantic import Field, model_validator | ||
|
|
||
| from bofire.data_models.features.api import AnyOutput, ContinuousOutput | ||
| from bofire.data_models.kernels.api import AnyKernel, RBFKernel, ScaleKernel | ||
| from bofire.data_models.priors.api import ( | ||
| PAIRWISEGP_LENGTHSCALE_CONSTRAINT, | ||
| PAIRWISEGP_LENGTHSCALE_PRIOR, | ||
| PAIRWISEGP_OUTPUTSCALE_CONSTRAINT, | ||
| PAIRWISEGP_OUTPUTSCALE_PRIOR, | ||
| ) | ||
| from bofire.data_models.surrogates.botorch import BotorchSurrogate | ||
| from bofire.data_models.surrogates.scaler import AnyScaler, Normalize | ||
| from bofire.data_models.surrogates.trainable import TrainableSurrogate | ||
|
|
||
|
|
||
| class PairwiseGPSurrogate(BotorchSurrogate, TrainableSurrogate): | ||
| """Pairwise Gaussian Process surrogate built on top of BoTorch's PairwiseGP. | ||
|
|
||
| Fits a latent utility function from binary winner/loser pair labels. The | ||
| `preferences` DataFrame references rows of the standard BoFire `experiments` | ||
| DataFrame by `labcode`; the single output feature represents the latent | ||
| utility inferred from those comparisons. | ||
| """ | ||
|
|
||
| type: Literal["PairwiseGPSurrogate"] = "PairwiseGPSurrogate" | ||
|
|
||
| kernel: AnyKernel = Field( | ||
| default_factory=lambda: ScaleKernel( | ||
| base_kernel=RBFKernel( | ||
| ard=True, | ||
| lengthscale_prior=PAIRWISEGP_LENGTHSCALE_PRIOR(), | ||
| lengthscale_constraint=PAIRWISEGP_LENGTHSCALE_CONSTRAINT(), | ||
| ), | ||
| outputscale_prior=PAIRWISEGP_OUTPUTSCALE_PRIOR(), | ||
| outputscale_constraint=PAIRWISEGP_OUTPUTSCALE_CONSTRAINT(), | ||
| ) | ||
| ) | ||
| scaler: AnyScaler = Field(default_factory=Normalize) | ||
|
|
||
| @classmethod | ||
| def is_output_implemented(cls, my_type: Type[AnyOutput]) -> bool: | ||
| return isinstance(my_type, type(ContinuousOutput)) | ||
|
|
||
| @model_validator(mode="after") | ||
|
jduerholt marked this conversation as resolved.
|
||
| def validate_single_output(self): | ||
| if len(self.outputs) != 1: | ||
| raise ValueError( | ||
| "PairwiseGPSurrogate supports exactly one output (the latent utility)." | ||
| ) | ||
| return self | ||
|
|
||
| @model_validator(mode="after") | ||
|
jduerholt marked this conversation as resolved.
|
||
| def validate_scalekernel(self): | ||
| if not isinstance(self.kernel, ScaleKernel): | ||
| raise ValueError( | ||
| "PairwiseGPSurrogate.kernel must be a ScaleKernel " | ||
| "(BoTorch's PairwiseGP requires the covariance module to be a ScaleKernel)." | ||
| ) | ||
| return self | ||
|
|
||
| @model_validator(mode="after") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need this? We do not have it anywhere else, or?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think, this is not needed and should also not be enforced here.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes you are right, will remove it. |
||
| def validate_scaler_features(self): | ||
| if self.scaler and len(self.scaler.features) > 0: | ||
| missing_features = list( | ||
| set(self.scaler.features) - set(self.inputs.get_keys()) | ||
| ) | ||
| if missing_features: | ||
| raise ValueError( | ||
| f"The following features are missing in inputs: {missing_features}" | ||
| ) | ||
| return self | ||
Uh oh!
There was an error while loading. Please reload this page.