Skip to content

Commit

Permalink
LSR-BO (#338)
Browse files Browse the repository at this point in the history
  • Loading branch information
jduerholt authored Jan 24, 2024
1 parent b274c13 commit 307ff65
Show file tree
Hide file tree
Showing 36 changed files with 1,447 additions and 172 deletions.
24 changes: 21 additions & 3 deletions bofire/benchmarks/single.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,31 @@ def _f(self, candidates: pd.DataFrame) -> pd.DataFrame:


class Branin(Benchmark):
def __init__(self, **kwargs) -> None:
def __init__(self, locality_factor: Optional[float] = None, **kwargs) -> None:
super().__init__(**kwargs)
self._domain = Domain(
inputs=Inputs(
features=[
ContinuousInput(key="x_1", bounds=(-5.0, 10)),
ContinuousInput(key="x_2", bounds=(0.0, 15.0)),
ContinuousInput(
key="x_1",
bounds=(-5.0, 10),
local_relative_bounds=(
0.5 * locality_factor,
0.5 * locality_factor,
)
if locality_factor is not None
else (math.inf, math.inf),
),
ContinuousInput(
key="x_2",
bounds=(0.0, 15.0),
local_relative_bounds=(
1.5 * locality_factor,
1.5 * locality_factor,
)
if locality_factor is not None
else (math.inf, math.inf),
),
]
),
outputs=Outputs(
Expand Down
92 changes: 41 additions & 51 deletions bofire/data_models/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,57 +13,47 @@
Input,
Output,
)
from bofire.data_models.kernels.api import AnyKernel, Kernel
from bofire.data_models.molfeatures.api import AnyMolFeatures, MolFeatures
from bofire.data_models.objectives.api import AnyObjective, Objective
from bofire.data_models.outlier_detection.api import (
AnyOutlierDetection,
OutlierDetection,
)
from bofire.data_models.priors.api import AnyPrior, Prior
from bofire.data_models.strategies.api import (
AnyCondition,
AnyLocalSearchConfig,
AnyPredictive,
AnySampler,
AnyStrategy,
PredictiveStrategy,
SamplerStrategy,
Strategy,
)
from bofire.data_models.surrogates.api import (
AnyBotorchSurrogate,
AnySurrogate,
BotorchSurrogate,
Surrogate,
)

try:
# in case of the minimal installation these import are not available
from bofire.data_models.kernels.api import AnyKernel, Kernel
from bofire.data_models.molfeatures.api import ( # noqa: F401
AnyMolFeatures,
MolFeatures,
)
from bofire.data_models.objectives.api import AnyObjective, Objective
from bofire.data_models.outlier_detection.api import (
AnyOutlierDetection,
OutlierDetection,
)
from bofire.data_models.priors.api import AnyPrior, Prior
from bofire.data_models.strategies.api import (
AnyCondition,
AnyPredictive,
AnySampler,
AnyStrategy,
PredictiveStrategy,
SamplerStrategy,
Strategy,
)
from bofire.data_models.surrogates.api import (
AnyBotorchSurrogate,
AnySurrogate,
BotorchSurrogate,
Surrogate,
)

data_model_list = [
AnyAcquisitionFunction,
AnyConstraint,
AnyFeature,
AnyKernel,
AnySurrogate,
AnyOutlierDetection,
AnyObjective,
AnyPrior,
AnyStrategy,
AnyMolFeatures,
Domain,
]
except ImportError:
data_model_list = [
AnyAcquisitionFunction,
AnyConstraint,
AnyFeature,
Domain,
]

AnyThing = [model for models in data_model_list for model in unions.to_list(models)]
data_model_list = [
AnyAcquisitionFunction,
AnyConstraint,
AnyFeature,
AnyKernel,
AnySurrogate,
AnyOutlierDetection,
AnyObjective,
AnyPrior,
AnyStrategy,
AnyMolFeatures,
Domain,
AnyLocalSearchConfig,
Inputs,
Outputs,
Constraints,
]

AnyThing = [model for models in data_model_list for model in unions.to_list(models)]
18 changes: 16 additions & 2 deletions bofire/data_models/domain/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ def get_bounds(
self,
specs: TInputTransformSpecs,
experiments: Optional[pd.DataFrame] = None,
reference_experiment: Optional[pd.Series] = None,
) -> Tuple[List[float], List[float]]:
"""Returns the boundaries of the optimization problem based on the transformations
defined in the `specs` dictionary.
Expand All @@ -537,6 +538,10 @@ def get_bounds(
experiments (Optional[pd.DataFrame], optional): Dataframe with input features.
If provided the real feature bounds are returned based on both the opt.
feature bounds and the extreme points in the dataframe. Defaults to None,
reference_experiment (Optional[pd.Serues], optional): If a reference experiment provided,
then the local bounds based on a local search region are provided as reference to the
reference experiment. Currently only supported for continuous inputs.
For more details, it is referred to https://www.merl.com/publications/docs/TR2023-057.pdf. Defaults to None.
Raises:
ValueError: If a feature type is not known.
Expand All @@ -545,15 +550,24 @@ def get_bounds(
Returns:
Tuple[List[float], List[float]]: list with lower bounds, list with upper bounds.
"""
if reference_experiment is not None and experiments is not None:
raise ValueError(
"Only one can be used, `reference_experiments` or `experiments`."
)

self._validate_transform_specs(specs=specs)

lower = []
upper = []

for feat in self.get():
lo, up = feat.get_bounds( # type: ignore
assert isinstance(feat, Input)
lo, up = feat.get_bounds(
transform_type=specs.get(feat.key), # type: ignore
values=experiments[feat.key] if experiments is not None else None, # type: ignore
values=experiments[feat.key] if experiments is not None else None,
reference_value=reference_experiment[feat.key] # type: ignore
if reference_experiment is not None
else None,
)
lower += lo
upper += up
Expand Down
1 change: 1 addition & 0 deletions bofire/data_models/features/categorical.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ def get_bounds(
self,
transform_type: TTransform,
values: Optional[pd.Series] = None,
reference_value: Optional[str] = None,
) -> Tuple[List[float], List[float]]:
assert isinstance(transform_type, CategoricalEncodingEnum)
if transform_type == CategoricalEncodingEnum.ORDINAL:
Expand Down
40 changes: 37 additions & 3 deletions bofire/data_models/features/continuous.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from typing import ClassVar, Literal, Optional, Tuple
import math
from typing import Annotated, ClassVar, List, Literal, Optional, Tuple

import numpy as np
import pandas as pd
from pydantic import Field, field_validator, model_validator

from bofire.data_models.features.feature import Output
from bofire.data_models.features.feature import Output, TTransform
from bofire.data_models.features.numerical import NumericalInput
from bofire.data_models.objectives.api import AnyObjective, MaximizeObjective

Expand All @@ -15,12 +16,17 @@ class ContinuousInput(NumericalInput):
Attributes:
bounds (Tuple[float, float]): A tuple that stores the lower and upper bound of the feature.
stepsize (float, optional): Float indicating the allowed stepsize between lower and upper. Defaults to None.
local_relative_bounds (Tuple[float, float], optional): A tuple that stores the lower and upper bounds relative to a reference value.
Defaults to (math.inf, math.inf).
"""

type: Literal["ContinuousInput"] = "ContinuousInput"
order_id: ClassVar[int] = 1

bounds: Tuple[float, float]
local_relative_bounds: Tuple[
Annotated[float, Field(gt=0)], Annotated[float, Field(gt=0)]
] = (math.inf, math.inf)
stepsize: Optional[float] = None

@property
Expand Down Expand Up @@ -65,7 +71,7 @@ def round(self, values: pd.Series) -> pd.Series:
allowed_values = np.arange(
self.lower_bound, self.upper_bound + self.stepsize, self.stepsize
)
idx = abs(values.values.reshape([len(values), 1]) - allowed_values).argmin(
idx = abs(values.values.reshape([len(values), 1]) - allowed_values).argmin( # type: ignore
axis=1
)
return pd.Series(
Expand Down Expand Up @@ -135,6 +141,34 @@ def sample(self, n: int, seed: Optional[int] = None) -> pd.Series:
),
)

def get_bounds(
self,
transform_type: Optional[TTransform] = None,
values: Optional[pd.Series] = None,
reference_value: Optional[float] = None,
) -> Tuple[List[float], List[float]]:
assert transform_type is None
if reference_value is not None and values is not None:
raise ValueError("Only one can be used, `local_value` or `values`.")
if values is None:
if reference_value is None or self.is_fixed():
return [self.lower_bound], [self.upper_bound]
else:
return [
max(
reference_value - self.local_relative_bounds[0],
self.lower_bound,
)
], [
min(
reference_value + self.local_relative_bounds[1],
self.upper_bound,
)
]
lower = min(self.lower_bound, values.min()) # type: ignore
upper = max(self.upper_bound, values.max()) # type: ignore
return [lower], [upper]

def __str__(self) -> str:
"""Method to return a string of lower and upper bound
Expand Down
5 changes: 4 additions & 1 deletion bofire/data_models/features/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,10 @@ def fixed_value(
return self.to_descriptor_encoding(pd.Series([val])).values[0].tolist()

def get_bounds(
self, transform_type: TTransform, values: Optional[pd.Series] = None
self,
transform_type: TTransform,
values: Optional[pd.Series] = None,
reference_value: Optional[str] = None,
) -> Tuple[List[float], List[float]]:
if transform_type != CategoricalEncodingEnum.DESCRIPTOR:
return super().get_bounds(transform_type, values)
Expand Down
17 changes: 15 additions & 2 deletions bofire/data_models/features/discrete.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import ClassVar, Literal, Optional
from typing import ClassVar, List, Literal, Optional, Tuple

import numpy as np
import pandas as pd
from pydantic import field_validator

from bofire.data_models.features.feature import TDiscreteVals
from bofire.data_models.features.feature import TDiscreteVals, TTransform
from bofire.data_models.features.numerical import NumericalInput


Expand Down Expand Up @@ -108,3 +108,16 @@ def from_continuous(self, values: pd.DataFrame) -> pd.Series:
).idxmin(1)
s.name = self.key
return s

def get_bounds(
self,
transform_type: Optional[TTransform] = None,
values: Optional[pd.Series] = None,
reference_value: Optional[float] = None,
) -> Tuple[List[float], List[float]]:
assert transform_type is None
if values is None:
return [self.lower_bound], [self.upper_bound] # type: ignore
lower = min(self.lower_bound, values.min()) # type: ignore
upper = max(self.upper_bound, values.max()) # type: ignore
return [lower], [upper] # type: ignore
7 changes: 5 additions & 2 deletions bofire/data_models/features/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from bofire.data_models.molfeatures.api import AnyMolFeatures
from bofire.data_models.surrogates.scaler import ScalerEnum

TTransform = Union[CategoricalEncodingEnum, ScalerEnum]
TTransform = Union[CategoricalEncodingEnum, ScalerEnum, AnyMolFeatures]


class Feature(BaseModel):
Expand Down Expand Up @@ -111,14 +111,17 @@ def get_bounds(
self,
transform_type: Optional[TTransform] = None,
values: Optional[pd.Series] = None,
reference_value: Optional[Union[float, str]] = None,
) -> Tuple[List[float], List[float]]:
"""Returns the bounds of an input feature depending on the requested transform type.
Args:
transform_type (Optional[TTransform], optional): The requested transform type. Defaults to None.
values (Optional[pd.Series], optional): If values are provided the bounds are returned taking
the most extreme values for the feature into account. Defaults to None.
reference_value (Optional[float], optional): If a reference value is provided, then the local bounds based
on a local search region are provided. Currently only supported for continuous inputs. For more
details, it is referred to https://www.merl.com/publications/docs/TR2023-057.pdf.
Returns:
Tuple[List[float], List[float]]: List of lower bound values, list of upper bound values.
"""
Expand Down
1 change: 1 addition & 0 deletions bofire/data_models/features/molecular.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ def get_bounds(
self,
transform_type: Union[CategoricalEncodingEnum, AnyMolFeatures],
values: Optional[pd.Series] = None,
reference_value: Optional[str] = None,
) -> Tuple[List[float], List[float]]:
if isinstance(transform_type, CategoricalEncodingEnum):
# we are just using the standard categorical transformations
Expand Down
14 changes: 1 addition & 13 deletions bofire/data_models/features/numerical.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Optional, Tuple, Union
from typing import List, Optional, Union

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -132,15 +132,3 @@ def validate_candidental(self, values: pd.Series) -> pd.Series:
f"not all values of input feature `{self.key}` are numerical"
)
return values

def get_bounds(
self,
transform_type: Optional[TTransform] = None,
values: Optional[pd.Series] = None,
) -> Tuple[List[float], List[float]]:
assert transform_type is None
if values is None:
return [self.lower_bound], [self.upper_bound] # type: ignore
lower = min(self.lower_bound, values.min()) # type: ignore
upper = max(self.upper_bound, values.max()) # type: ignore
return [lower], [upper] # type: ignore
7 changes: 6 additions & 1 deletion bofire/data_models/strategies/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from bofire.data_models.strategies.doe import DoEStrategy
from bofire.data_models.strategies.factorial import FactorialStrategy
from bofire.data_models.strategies.predictives.botorch import BotorchStrategy
from bofire.data_models.strategies.predictives.botorch import LSRBO, BotorchStrategy
from bofire.data_models.strategies.predictives.mobo import MoboStrategy
from bofire.data_models.strategies.predictives.multiobjective import (
MultiobjectiveStrategy,
Expand All @@ -24,6 +24,7 @@
from bofire.data_models.strategies.samplers.universal_constraint import (
UniversalConstraintSampler,
)
from bofire.data_models.strategies.shortest_path import ShortestPathStrategy
from bofire.data_models.strategies.stepwise.conditions import ( # noqa: F401
AlwaysTrueCondition,
CombiCondition,
Expand Down Expand Up @@ -59,6 +60,7 @@
StepwiseStrategy,
FactorialStrategy,
MoboStrategy,
ShortestPathStrategy,
]

AnyPredictive = Union[
Expand All @@ -76,3 +78,6 @@


AnyCondition = Union[NumberOfExperimentsCondition, CombiCondition, AlwaysTrueCondition]


AnyLocalSearchConfig = LSRBO
Loading

0 comments on commit 307ff65

Please sign in to comment.