diff --git a/bofire/data_models/domain/domain.py b/bofire/data_models/domain/domain.py index fbfaf91f..ffe3ee8c 100644 --- a/bofire/data_models/domain/domain.py +++ b/bofire/data_models/domain/domain.py @@ -21,6 +21,7 @@ from bofire.data_models.constraints.api import ( AnyConstraint, ConstraintNotFulfilledError, + InterpointEqualityConstraint, LinearConstraint, NChooseKConstraint, ProductConstraint, @@ -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") diff --git a/bofire/data_models/strategies/predictives/botorch.py b/bofire/data_models/strategies/predictives/botorch.py index b133ca9e..c161f275 100644 --- a/bofire/data_models/strategies/predictives/botorch.py +++ b/bofire/data_models/strategies/predictives/botorch.py @@ -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 @@ -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""" diff --git a/bofire/data_models/strategies/predictives/qparego.py b/bofire/data_models/strategies/predictives/qparego.py index 9d228d4d..ce086d1b 100644 --- a/bofire/data_models/strategies/predictives/qparego.py +++ b/bofire/data_models/strategies/predictives/qparego.py @@ -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, @@ -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 diff --git a/bofire/strategies/predictives/botorch.py b/bofire/strategies/predictives/botorch.py index fb1e8990..e12fec99 100644 --- a/bofire/strategies/predictives/botorch.py +++ b/bofire/strategies/predictives/botorch.py @@ -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, @@ -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, @@ -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 diff --git a/bofire/utils/torch_tools.py b/bofire/utils/torch_tools.py index 8124dfc3..ca0e73f5 100644 --- a/bofire/utils/torch_tools.py +++ b/bofire/utils/torch_tools.py @@ -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) diff --git a/tests/bofire/data_models/specs/domain.py b/tests/bofire/data_models/specs/domain.py index 351cc95e..a1389495 100644 --- a/tests/bofire/data_models/specs/domain.py +++ b/tests/bofire/data_models/specs/domain.py @@ -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 @@ -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.", +) diff --git a/tests/bofire/data_models/specs/strategies.py b/tests/bofire/data_models/specs/strategies.py index b4756d25..fbb1f844 100644 --- a/tests/bofire/data_models/specs/strategies.py +++ b/tests/bofire/data_models/specs/strategies.py @@ -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, @@ -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: { diff --git a/tests/bofire/strategies/test_sobo.py b/tests/bofire/strategies/test_sobo.py index b37e7ed6..a8dbbb67 100644 --- a/tests/bofire/strategies/test_sobo.py +++ b/tests/bofire/strategies/test_sobo.py @@ -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 ( @@ -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) diff --git a/tests/bofire/utils/test_torch_tools.py b/tests/bofire/utils/test_torch_tools.py index 2ea6e17c..fb637a5e 100644 --- a/tests/bofire/utils/test_torch_tools.py +++ b/tests/bofire/utils/test_torch_tools.py @@ -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():