Skip to content

Commit

Permalink
add interpoint to botorch (#426)
Browse files Browse the repository at this point in the history
  • Loading branch information
jduerholt authored Aug 16, 2024
1 parent f69dda0 commit ba1098b
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 3 deletions.
4 changes: 4 additions & 0 deletions bofire/data_models/domain/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from bofire.data_models.constraints.api import (
AnyConstraint,
ConstraintNotFulfilledError,
InterpointEqualityConstraint,
LinearConstraint,
NChooseKConstraint,
ProductConstraint,
Expand Down Expand Up @@ -154,6 +155,9 @@ def validate_constraints(self):
for f in c.features: # type: ignore
if f not in keys:
raise ValueError(f"feature {f} in constraint unknown ({keys})")
for c in self.constraints.get(InterpointEqualityConstraint):
if c.feature not in keys:
raise ValueError(f"feature {c.feature} not known.")
return self

@model_validator(mode="after")
Expand Down
17 changes: 16 additions & 1 deletion bofire/data_models/strategies/predictives/botorch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
from bofire.data_models.base import BaseModel
from bofire.data_models.constraints.api import (
Constraint,
InterpointConstraint,
LinearConstraint,
NonlinearEqualityConstraint,
NonlinearInequalityConstraint,
)
from bofire.data_models.domain.api import Domain, Outputs
from bofire.data_models.enum import CategoricalEncodingEnum, CategoricalMethodEnum
from bofire.data_models.features.api import CategoricalDescriptorInput, CategoricalInput
from bofire.data_models.features.api import (
CategoricalDescriptorInput,
CategoricalInput,
ContinuousInput,
)
from bofire.data_models.outlier_detection.api import OutlierDetections
from bofire.data_models.strategies.predictives.predictive import PredictiveStrategy
from bofire.data_models.strategies.shortest_path import has_local_search_region
Expand Down Expand Up @@ -126,6 +131,16 @@ def is_constraint_implemented(cls, my_type: Type[Constraint]) -> bool:
return False
return True

@model_validator(mode="after")
def validate_interpoint_constraints(self):
if self.domain.constraints.get(InterpointConstraint) and len(
self.domain.inputs.get(ContinuousInput)
) != len(self.domain.inputs):
raise ValueError(
"Interpoint constraints can only be used for pure continuous search spaces."
)
return self

@model_validator(mode="after")
def validate_surrogate_specs(self):
"""Ensures that a prediction model is specified for each output feature"""
Expand Down
24 changes: 24 additions & 0 deletions bofire/data_models/strategies/predictives/qparego.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
from pydantic import Field

from bofire.data_models.acquisition_functions.api import qEI, qLogEI, qLogNEI, qNEI
from bofire.data_models.constraints.api import (
Constraint,
InterpointConstraint,
NonlinearEqualityConstraint,
NonlinearInequalityConstraint,
)
from bofire.data_models.features.api import Feature
from bofire.data_models.objectives.api import (
CloseToTargetObjective,
Expand Down Expand Up @@ -40,3 +46,21 @@ def is_objective_implemented(cls, my_type: Type[Objective]) -> bool:
]:
return False
return True

@classmethod
def is_constraint_implemented(cls, my_type: Type[Constraint]) -> bool:
"""Method to check if a specific constraint type is implemented for the strategy
Args:
my_type (Type[Constraint]): Constraint class
Returns:
bool: True if the constraint type is valid for the strategy chosen, False otherwise
"""
if my_type in [
NonlinearInequalityConstraint,
NonlinearEqualityConstraint,
InterpointConstraint,
]:
return False
return True
7 changes: 6 additions & 1 deletion bofire/strategies/predictives/botorch.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from bofire.surrogates.botorch_surrogates import BotorchSurrogates
from bofire.utils.torch_tools import (
get_initial_conditions_generator,
get_interpoint_constraints,
get_linear_constraints,
get_nonlinear_constraints,
tkwargs,
Expand Down Expand Up @@ -358,6 +359,9 @@ def _optimize_acqf_continuous(
options=self._get_optimizer_options(), # type: ignore
)
else:
interpoints = get_interpoint_constraints(
domain=self.domain, n_candidates=candidate_count
)
candidates, acqf_vals = optimize_acqf(
acq_function=acqfs[0],
bounds=bounds,
Expand All @@ -367,7 +371,8 @@ def _optimize_acqf_continuous(
equality_constraints=get_linear_constraints(
domain=self.domain,
constraint=LinearEqualityConstraint, # type: ignore
),
)
+ interpoints,
inequality_constraints=get_linear_constraints(
domain=self.domain,
constraint=LinearInequalityConstraint, # type: ignore
Expand Down
2 changes: 2 additions & 0 deletions bofire/utils/torch_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ def get_interpoint_constraints(
of a tensor with the feature indices, coefficients and a float for the rhs.
"""
constraints = []
if n_candidates == 1:
return constraints
for constraint in domain.constraints.get(InterpointEqualityConstraint):
assert isinstance(constraint, InterpointEqualityConstraint)
coefficients = torch.tensor([1.0, -1.0]).to(**tkwargs)
Expand Down
23 changes: 23 additions & 0 deletions tests/bofire/data_models/specs/domain.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from bofire.data_models.constraints.api import InterpointEqualityConstraint
from bofire.data_models.domain.api import Constraints, Domain, Inputs, Outputs
from bofire.data_models.features.api import ContinuousInput, ContinuousOutput
from tests.bofire.data_models.specs.features import specs as features
Expand Down Expand Up @@ -71,3 +72,25 @@
error=ValueError,
message="Feature keys are not unique",
)

specs.add_invalid(
Domain,
lambda: {
"inputs": Inputs(
features=[
features.valid(ContinuousInput).obj(key="i1"),
features.valid(ContinuousInput).obj(key="i2"),
]
),
"outputs": Outputs(
features=[
features.valid(ContinuousOutput).obj(key="o1"),
]
),
"constraints": Constraints(
constraints=[InterpointEqualityConstraint(feature="i3")]
),
},
error=ValueError,
message="feature i3 not known.",
)
24 changes: 24 additions & 0 deletions tests/bofire/data_models/specs/strategies.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import bofire.data_models.strategies.api as strategies
from bofire.data_models.acquisition_functions.api import qEI, qLogNEHVI, qPI
from bofire.data_models.constraints.api import (
InterpointEqualityConstraint,
LinearEqualityConstraint,
LinearInequalityConstraint,
NChooseKConstraint,
Expand Down Expand Up @@ -416,6 +417,29 @@
message="LSR-BO only supported for linear constraints.",
)

specs.add_invalid(
strategies.SoboStrategy,
lambda: {
"domain": Domain(
inputs=Inputs(
features=[
ContinuousInput(
key=k, bounds=(0, 1), local_relative_bounds=(0.1, 0.1)
)
for k in ["a", "b", "c"]
]
+ [CategoricalInput(key="d", categories=["a", "b", "c"])]
),
outputs=Outputs(features=[ContinuousOutput(key="alpha")]),
constraints=Constraints(
constraints=[InterpointEqualityConstraint(feature="a")]
),
).model_dump(),
},
error=ValueError,
message="Interpoint constraints can only be used for pure continuous search spaces.",
)

specs.add_valid(
strategies.FractionalFactorialStrategy,
lambda: {
Expand Down
16 changes: 15 additions & 1 deletion tests/bofire/strategies/test_sobo.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
qSR,
qUCB,
)
from bofire.data_models.constraints.api import NChooseKConstraint
from bofire.data_models.constraints.api import (
InterpointEqualityConstraint,
NChooseKConstraint,
)
from bofire.data_models.domain.api import Domain, Inputs, Outputs
from bofire.data_models.features.api import ContinuousInput, ContinuousOutput
from bofire.data_models.objectives.api import (
Expand Down Expand Up @@ -478,3 +481,14 @@ def test_sobo_get_optimizer_options():
strategy_data = data_models.SoboStrategy(domain=domain, maxiter=500, batch_limit=4)
strategy = SoboStrategy(data_model=strategy_data)
assert strategy._get_optimizer_options() == {"maxiter": 500, "batch_limit": 1}


def test_sobo_interpoint():
bench = Himmelblau()
experiments = bench.f(bench.domain.inputs.sample(4), return_complete=True)
domain = bench._domain
domain.constraints.constraints.append(InterpointEqualityConstraint(feature="x_1"))
strategy_data = data_models.SoboStrategy(domain=domain)
strategy = SoboStrategy(data_model=strategy_data)
strategy.tell(experiments)
strategy.ask(2)
2 changes: 2 additions & 0 deletions tests/bofire/utils/test_torch_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,8 @@ def test_get_interpoint_equality_constraints():
dtype=torch.int64,
),
)
constraints = get_interpoint_constraints(domain=domain, n_candidates=1)
assert len(constraints) == 0


def test_get_linear_constraints():
Expand Down

0 comments on commit ba1098b

Please sign in to comment.