diff --git a/.gitignore b/.gitignore index eb8e1e93c..6df9fafed 100644 --- a/.gitignore +++ b/.gitignore @@ -116,6 +116,7 @@ venv.bak/ # vscode project settings .vscode/ +.devcontainer # Rope project settings .ropeproject @@ -133,3 +134,4 @@ dmypy.json # generated version file bofire/version.py + diff --git a/bofire/data_models/strategies/api.py b/bofire/data_models/strategies/api.py index e982f9d2b..a2d692361 100644 --- a/bofire/data_models/strategies/api.py +++ b/bofire/data_models/strategies/api.py @@ -22,6 +22,7 @@ from bofire.data_models.strategies.space_filling import SpaceFillingStrategy from bofire.data_models.strategies.stepwise.conditions import ( # noqa: F401 AlwaysTrueCondition, + AnyCondition, CombiCondition, NumberOfExperimentsCondition, ) @@ -30,6 +31,10 @@ StepwiseStrategy, ) from bofire.data_models.strategies.strategy import Strategy +from bofire.data_models.transforms.api import ( # noqa: F401 + AnyTransform, + DropDataTransform, +) AbstractStrategy = Union[ Strategy, @@ -67,7 +72,4 @@ ] -AnyCondition = Union[NumberOfExperimentsCondition, CombiCondition, AlwaysTrueCondition] - - AnyLocalSearchConfig = LSRBO diff --git a/bofire/data_models/strategies/stepwise/conditions.py b/bofire/data_models/strategies/stepwise/conditions.py index 837a9c986..9c8a042a3 100644 --- a/bofire/data_models/strategies/stepwise/conditions.py +++ b/bofire/data_models/strategies/stepwise/conditions.py @@ -1,9 +1,18 @@ -from typing import List, Literal, Union +from abc import abstractmethod +from typing import List, Literal, Optional, Union +import pandas as pd from pydantic import Field, field_validator from typing_extensions import Annotated from bofire.data_models.base import BaseModel +from bofire.data_models.domain.api import Domain + + +class EvaluateableCondition: + @abstractmethod + def evaluate(self, domain: Domain, experiments: Optional[pd.DataFrame]) -> bool: + pass class Condition(BaseModel): @@ -14,16 +23,28 @@ class SingleCondition(BaseModel): type: str -class NumberOfExperimentsCondition(SingleCondition): +class NumberOfExperimentsCondition(SingleCondition, EvaluateableCondition): type: Literal["NumberOfExperimentsCondition"] = "NumberOfExperimentsCondition" n_experiments: Annotated[int, Field(ge=1)] + def evaluate(self, domain: Domain, experiments: Optional[pd.DataFrame]) -> bool: + if experiments is None: + n_experiments = 0 + else: + n_experiments = len( + domain.outputs.preprocess_experiments_all_valid_outputs(experiments) + ) + return n_experiments <= self.n_experiments + -class AlwaysTrueCondition(SingleCondition): +class AlwaysTrueCondition(SingleCondition, EvaluateableCondition): type: Literal["AlwaysTrueCondition"] = "AlwaysTrueCondition" + def evaluate(self, domain: Domain, experiments: Optional[pd.DataFrame]) -> bool: + return True -class CombiCondition(Condition): + +class CombiCondition(Condition, EvaluateableCondition): type: Literal["CombiCondition"] = "CombiCondition" conditions: Annotated[ List[ @@ -41,3 +62,15 @@ def validate_n_required_conditions(cls, v, info): "Number of required conditions larger than number of conditions." ) return v + + def evaluate(self, domain: Domain, experiments: Optional[pd.DataFrame]) -> bool: + n_matched_conditions = 0 + for c in self.conditions: + if c.evaluate(domain, experiments): + n_matched_conditions += 1 + if n_matched_conditions >= self.n_required_conditions: + return True + return False + + +AnyCondition = Union[NumberOfExperimentsCondition, CombiCondition, AlwaysTrueCondition] diff --git a/bofire/data_models/strategies/stepwise/stepwise.py b/bofire/data_models/strategies/stepwise/stepwise.py index 8f39953f3..a4d06ad3f 100644 --- a/bofire/data_models/strategies/stepwise/stepwise.py +++ b/bofire/data_models/strategies/stepwise/stepwise.py @@ -1,4 +1,4 @@ -from typing import List, Literal, Type, Union +from typing import List, Literal, Optional, Type, Union from pydantic import Field, field_validator from typing_extensions import Annotated @@ -6,6 +6,7 @@ from bofire.data_models.base import BaseModel from bofire.data_models.constraints.api import Constraint from bofire.data_models.features.api import Feature +from bofire.data_models.strategies.api import AnyCondition from bofire.data_models.strategies.doe import DoEStrategy from bofire.data_models.strategies.factorial import FactorialStrategy from bofire.data_models.strategies.predictives.mobo import MoboStrategy @@ -21,12 +22,9 @@ from bofire.data_models.strategies.random import RandomStrategy from bofire.data_models.strategies.shortest_path import ShortestPathStrategy from bofire.data_models.strategies.space_filling import SpaceFillingStrategy -from bofire.data_models.strategies.stepwise.conditions import ( - AlwaysTrueCondition, - CombiCondition, - NumberOfExperimentsCondition, -) +from bofire.data_models.strategies.stepwise.conditions import AlwaysTrueCondition from bofire.data_models.strategies.strategy import Strategy +from bofire.data_models.transforms.api import AnyTransform AnyStrategy = Union[ SoboStrategy, @@ -44,14 +42,12 @@ ShortestPathStrategy, ] -AnyCondition = Union[NumberOfExperimentsCondition, CombiCondition, AlwaysTrueCondition] - class Step(BaseModel): type: Literal["Step"] = "Step" strategy_data: AnyStrategy condition: AnyCondition - max_parallelism: Annotated[int, Field(ge=-1)] # -1 means no restriction at all + transform: Optional[AnyTransform] = None class StepwiseStrategy(Strategy): diff --git a/bofire/data_models/transforms/__init__.py b/bofire/data_models/transforms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/bofire/data_models/transforms/api.py b/bofire/data_models/transforms/api.py new file mode 100644 index 000000000..612acee21 --- /dev/null +++ b/bofire/data_models/transforms/api.py @@ -0,0 +1,3 @@ +from bofire.data_models.transforms.drop_data import DropDataTransform + +AnyTransform = DropDataTransform diff --git a/bofire/data_models/transforms/drop_data.py b/bofire/data_models/transforms/drop_data.py new file mode 100644 index 000000000..cd836c564 --- /dev/null +++ b/bofire/data_models/transforms/drop_data.py @@ -0,0 +1,9 @@ +from typing import List, Literal, Optional + +from pydantic import BaseModel + + +class DropDataTransform(BaseModel): + type: Literal["DropDataTransform"] = "DropDataTransform" + to_be_removed_experiments: Optional[List[int]] = None + to_be_removed_candidates: Optional[List[int]] = None diff --git a/bofire/strategies/stepwise/conditions.py b/bofire/strategies/stepwise/conditions.py deleted file mode 100644 index 37b8673c3..000000000 --- a/bofire/strategies/stepwise/conditions.py +++ /dev/null @@ -1,67 +0,0 @@ -from abc import abstractmethod -from typing import Union - -import pandas as pd - -import bofire.data_models.strategies.stepwise.conditions as data_models -from bofire.data_models.domain.api import Domain - - -class Condition(object): - @abstractmethod - def evaluate(self, domain: Domain, experiments: pd.DataFrame) -> bool: - pass - - -class NumberOfExperimentsCondition(Condition): - def __init__(self, data_model: data_models.NumberOfExperimentsCondition): - self.n_experiments = data_model.n_experiments - - def evaluate(self, domain: Domain, experiments: Union[pd.DataFrame, None]) -> bool: - if experiments is None: - n_experiments = 0 - else: - n_experiments = len( - domain.outputs.preprocess_experiments_all_valid_outputs(experiments) - ) - return n_experiments <= self.n_experiments - - -class AlwaysTrueCondition(Condition): - def __init__(self, data_model: data_models.AlwaysTrueCondition): - pass - - def evaluate(self, domain: Domain, experiments: pd.DataFrame) -> bool: - return True - - -class CombiCondition(Condition): - def __init__(self, data_model: data_models.CombiCondition) -> None: - self.conditions = [map(c) for c in data_model.conditions] # type: ignore - self.n_required_conditions = data_model.n_required_conditions - - def evaluate(self, domain: Domain, experiments: pd.DataFrame) -> bool: - n_matched_conditions = 0 - for c in self.conditions: - if c.evaluate(domain, experiments): - n_matched_conditions += 1 - if n_matched_conditions >= self.n_required_conditions: - return True - return False - - -CONDITION_MAP = { - data_models.CombiCondition: CombiCondition, - data_models.NumberOfExperimentsCondition: NumberOfExperimentsCondition, - data_models.AlwaysTrueCondition: AlwaysTrueCondition, -} - - -def map( - data_model: Union[ - data_models.CombiCondition, - data_models.NumberOfExperimentsCondition, - data_models.AlwaysTrueCondition, - ], -) -> Union[CombiCondition, NumberOfExperimentsCondition, AlwaysTrueCondition]: - return CONDITION_MAP[data_model.__class__](data_model) diff --git a/bofire/strategies/stepwise/stepwise.py b/bofire/strategies/stepwise/stepwise.py index b234b1ed5..6294811f3 100644 --- a/bofire/strategies/stepwise/stepwise.py +++ b/bofire/strategies/stepwise/stepwise.py @@ -1,10 +1,11 @@ -from typing import Dict, Tuple, Type +from typing import Dict, Literal, Optional, Tuple, Type, TypeVar, Union import pandas as pd +from pydantic import PositiveInt import bofire.data_models.strategies.api as data_models -import bofire.strategies.stepwise.conditions as conditions -from bofire.data_models.strategies.api import Step +import bofire.transforms.api as transforms +from bofire.data_models.domain.api import Domain from bofire.data_models.strategies.api import StepwiseStrategy as data_model from bofire.strategies.doe_strategy import DoEStrategy from bofire.strategies.factorial import FactorialStrategy @@ -22,6 +23,7 @@ from bofire.strategies.shortest_path import ShortestPathStrategy from bofire.strategies.space_filling import SpaceFillingStrategy from bofire.strategies.strategy import Strategy +from bofire.transforms.transform import Transform # we have to duplicate the map functionality due to prevent circular imports STRATEGY_MAP: Dict[Type[data_models.Strategy], Type[Strategy]] = { @@ -41,40 +43,67 @@ } -def map(data_model: data_models.Strategy) -> Strategy: +def _map(data_model: data_models.Strategy) -> Strategy: cls = STRATEGY_MAP[data_model.__class__] return cls.from_spec(data_model=data_model) +T = TypeVar("T", pd.DataFrame, Domain) + + +TfData = Union[Literal["experiments"], Literal["candidates"], Literal["domain"]] + + +def _apply_tf( + data: Optional[T], + transform: Optional[Transform], + tf: TfData, +) -> Optional[T]: + if data is not None and transform is not None: + return getattr(transform, f"transform_{tf}")(data) + + class StepwiseStrategy(Strategy): def __init__(self, data_model: data_model, **kwargs): super().__init__(data_model, **kwargs) - self.steps = data_model.steps + self.strategies = [_map(s.strategy_data) for s in data_model.steps] + self.conditions = [s.condition for s in data_model.steps] + self.transforms = [ + s.transform and transforms.map(s.transform) for s in data_model.steps + ] def has_sufficient_experiments(self) -> bool: return True - def _get_step(self) -> Tuple[int, Step]: # type: ignore - for i, step in enumerate(self.steps): - condition = conditions.map(step.condition) + def _get_step(self) -> Tuple[Strategy, Optional[Transform]]: + """Returns the strategy at the current step and the corresponding transform if given.""" + for i, condition in enumerate(self.conditions): if condition.evaluate(self.domain, experiments=self.experiments): - return i, step + return self.strategies[i], self.transforms[i] raise ValueError("No condition could be satisfied.") - def _ask(self, candidate_count: int) -> pd.DataFrame: - # we have to decide here w - istep, step = self._get_step() - if (step.max_parallelism > 0) and (candidate_count > step.max_parallelism): - raise ValueError( - f"Maximum number of candidates for step {istep} is {step.max_parallelism}." - ) - # map it - strategy = map(step.strategy_data) + def _ask(self, candidate_count: Optional[PositiveInt]) -> pd.DataFrame: + strategy, transform = self._get_step() + + candidate_count = candidate_count or 1 + + # handle a possible transform + tf_domain = _apply_tf(self.domain, transform, "domain") + transformed_domain = tf_domain or self.domain + strategy.domain = transformed_domain + tf_exp = _apply_tf(self.experiments, transform, "experiments") + transformed_experiments = self.experiments if tf_exp is None else tf_exp + tf_cand = _apply_tf(self.candidates, transform, "candidates") + transformed_candidates = self.candidates if tf_cand is None else tf_cand # tell the experiments - if self.num_experiments > 0: - strategy.tell(experiments=self.experiments) + if transformed_experiments is not None and len(transformed_experiments) > 0: + strategy.tell(experiments=transformed_experiments, replace=True) # tell pending - if self.num_candidates > 0: - strategy.set_candidates(self.candidates) + if transformed_candidates is not None and len(transformed_candidates) > 0: + strategy.set_candidates(transformed_candidates) # ask and return - return strategy.ask(candidate_count=candidate_count) + candidates = strategy.ask(candidate_count=candidate_count) + if transform is not None: + return transform.untransform_candidates(candidates) + else: + return candidates diff --git a/bofire/transforms/__init__.py b/bofire/transforms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/bofire/transforms/api.py b/bofire/transforms/api.py new file mode 100644 index 000000000..7a4252ff3 --- /dev/null +++ b/bofire/transforms/api.py @@ -0,0 +1,13 @@ +from typing import Dict, Type + +import bofire.data_models.transforms.api as data_models +from bofire.transforms.drop_data import DropDataTransform +from bofire.transforms.transform import Transform + +TRANSFORM_MAP: Dict[Type[data_models.AnyTransform], Type[Transform]] = { + data_models.DropDataTransform: DropDataTransform +} + + +def map(data_model: data_models.AnyTransform) -> Transform: + return TRANSFORM_MAP[data_model.__class__](data_model) diff --git a/bofire/transforms/drop_data.py b/bofire/transforms/drop_data.py new file mode 100644 index 000000000..5cb6d034e --- /dev/null +++ b/bofire/transforms/drop_data.py @@ -0,0 +1,16 @@ +import pandas as pd + +from bofire.data_models.transforms.api import DropDataTransform as DataModel +from bofire.transforms.transform import Transform + + +class DropDataTransform(Transform): + def __init__(self, data_model: DataModel): + self.to_be_removed_experiments = data_model.to_be_removed_experiments or [] + self.to_be_removed_candidates = data_model.to_be_removed_candidates or [] + + def transform_experiments(self, experiments: pd.DataFrame) -> pd.DataFrame: + return experiments.drop(self.to_be_removed_experiments) + + def transform_candidates(self, candidates: pd.DataFrame) -> pd.DataFrame: + return candidates.drop(self.to_be_removed_candidates) diff --git a/bofire/transforms/transform.py b/bofire/transforms/transform.py new file mode 100644 index 000000000..b68a5c453 --- /dev/null +++ b/bofire/transforms/transform.py @@ -0,0 +1,20 @@ +import pandas as pd + +from bofire.data_models.domain.api import Domain + + +class Transform: + def __init__(self, *_args, **_kwargs) -> None: + pass + + def transform_experiments(self, experiments: pd.DataFrame) -> pd.DataFrame: + return experiments + + def transform_candidates(self, candidates: pd.DataFrame) -> pd.DataFrame: + return candidates + + def transform_domain(self, domain: Domain) -> Domain: + return domain + + def untransform_candidates(self, experiments: pd.DataFrame) -> pd.DataFrame: + return experiments diff --git a/tests/bofire/conftest.py b/tests/bofire/conftest.py index a114a4986..3d13eefca 100644 --- a/tests/bofire/conftest.py +++ b/tests/bofire/conftest.py @@ -74,6 +74,11 @@ def invalid_condition_spec(request) -> specs.InvalidSpec: return request.param +@fixture(params=specs.transforms.invalids) +def invalid_transforms_spec(request) -> specs.InvalidSpec: + return request.param + + @fixture(params=specs.outlier_detection.invalids) def invalid_outlier_detection_spec(request) -> specs.InvalidSpec: return request.param @@ -150,6 +155,11 @@ def condition_spec(request) -> specs.Spec: return request.param +@fixture(params=specs.transforms.valids) +def transforms_detection_spec(request) -> specs.Spec: + return request.param + + @fixture(params=specs.outlier_detection.valids) def outlier_detection_spec(request) -> specs.Spec: return request.param diff --git a/tests/bofire/data_models/specs/api.py b/tests/bofire/data_models/specs/api.py index 9387ccdc2..e095e91e1 100644 --- a/tests/bofire/data_models/specs/api.py +++ b/tests/bofire/data_models/specs/api.py @@ -21,3 +21,4 @@ from tests.bofire.data_models.specs.specs import InvalidSpec, Spec, Specs from tests.bofire.data_models.specs.strategies import specs as strategies from tests.bofire.data_models.specs.surrogates import specs as surrogates +from tests.bofire.data_models.specs.transforms import specs as transforms diff --git a/tests/bofire/data_models/specs/strategies.py b/tests/bofire/data_models/specs/strategies.py index bc6c11944..0172ae7d6 100644 --- a/tests/bofire/data_models/specs/strategies.py +++ b/tests/bofire/data_models/specs/strategies.py @@ -149,14 +149,12 @@ strategies.Step( strategy_data=strategies.RandomStrategy(domain=tempdomain), condition=strategies.NumberOfExperimentsCondition(n_experiments=10), - max_parallelism=2, ).model_dump(), strategies.Step( strategy_data=strategies.QehviStrategy( domain=tempdomain, ), condition=strategies.NumberOfExperimentsCondition(n_experiments=30), - max_parallelism=2, ).model_dump(), ], "seed": 42, diff --git a/tests/bofire/data_models/specs/transforms.py b/tests/bofire/data_models/specs/transforms.py new file mode 100644 index 000000000..c7f89d94d --- /dev/null +++ b/tests/bofire/data_models/specs/transforms.py @@ -0,0 +1,36 @@ +from pydantic import ValidationError + +from bofire.data_models.strategies.api import DropDataTransform +from tests.bofire.data_models.specs.specs import Specs + +specs = Specs([]) + +specs.add_valid( + DropDataTransform, + lambda: { + "to_be_removed_experiments": [1, 2, 3], + "to_be_removed_candidates": [4, 5, 6], + }, +) + +specs.add_valid( + DropDataTransform, + lambda: {}, +) +specs.add_valid( + DropDataTransform, + lambda: {"to_be_removed_candidates": [4, 5, 6]}, +) +specs.add_valid( + DropDataTransform, + lambda: {"to_be_removed_experiments": None, "to_be_removed_candidates": [4, 5, 6]}, +) +specs.add_valid( + DropDataTransform, + lambda: {"to_be_removed_experiments": [1, 2, 3], "to_be_removed_candidates": None}, +) +specs.add_invalid( + DropDataTransform, + lambda: {"to_be_removed_exp": None, "to_be_removed_cand": None}, + error=ValidationError, +) diff --git a/tests/bofire/data_models/test_invalid.py b/tests/bofire/data_models/test_invalid.py index 5f84c4303..942ae479e 100644 --- a/tests/bofire/data_models/test_invalid.py +++ b/tests/bofire/data_models/test_invalid.py @@ -62,6 +62,10 @@ def test_condition_should_be_invalid(invalid_condition_spec: InvalidSpec): _invalidate(invalid_condition_spec) +def test_transform_should_be_invalid(invalid_condition_spec: InvalidSpec): + _invalidate(invalid_condition_spec) + + def test_outlier_detection_should_be_invalid( invalid_outlier_detection_spec: InvalidSpec, ): diff --git a/tests/bofire/strategies/stepwise/test_conditions.py b/tests/bofire/strategies/stepwise/test_conditions.py index 0c9b741bd..5c480e347 100644 --- a/tests/bofire/strategies/stepwise/test_conditions.py +++ b/tests/bofire/strategies/stepwise/test_conditions.py @@ -1,31 +1,27 @@ import pytest import bofire.data_models.strategies.api as data_models -import bofire.strategies.stepwise.conditions as conditions from bofire.benchmarks.single import Himmelblau def test_RequiredExperimentsCondition(): benchmark = Himmelblau() experiments = benchmark.f(benchmark.domain.inputs.sample(3), return_complete=True) - data_model = data_models.NumberOfExperimentsCondition(n_experiments=3) - condition = conditions.map(data_model=data_model) + condition = data_models.NumberOfExperimentsCondition(n_experiments=3) assert condition.evaluate(benchmark.domain, experiments=experiments) is True experiments = benchmark.f(benchmark.domain.inputs.sample(10), return_complete=True) assert condition.evaluate(benchmark.domain, experiments=experiments) is False def test_RequiredExperimentsCondition_no_experiments(): - data_model = data_models.NumberOfExperimentsCondition(n_experiments=3) - condition = conditions.map(data_model=data_model) + condition = data_models.NumberOfExperimentsCondition(n_experiments=3) assert condition.evaluate(Himmelblau().domain, experiments=None) is True def test_AlwaysTrueCondition(): benchmark = Himmelblau() experiments = benchmark.f(benchmark.domain.inputs.sample(3), return_complete=True) - data_model = data_models.AlwaysTrueCondition() - condition = conditions.map(data_model=data_model) + condition = data_models.AlwaysTrueCondition() assert condition.evaluate(benchmark.domain, experiments=experiments) is True @@ -51,12 +47,11 @@ def test_CombiCondition(n_required, n_experiments, expected): experiments = benchmark.f( benchmark.domain.inputs.sample(n_experiments), return_complete=True ) - data_model = data_models.CombiCondition( + condition = data_models.CombiCondition( conditions=[ data_models.NumberOfExperimentsCondition(n_experiments=2), data_models.NumberOfExperimentsCondition(n_experiments=12), ], n_required_conditions=n_required, ) - condition = conditions.map(data_model=data_model) assert condition.evaluate(benchmark.domain, experiments=experiments) is expected diff --git a/tests/bofire/strategies/stepwise/test_stepwise.py b/tests/bofire/strategies/stepwise/test_stepwise.py index 10f3a8cbb..8b162d0fa 100644 --- a/tests/bofire/strategies/stepwise/test_stepwise.py +++ b/tests/bofire/strategies/stepwise/test_stepwise.py @@ -1,4 +1,5 @@ from copy import deepcopy +from typing import cast import pytest @@ -30,14 +31,12 @@ def test_StepwiseStrategy_invalid_domains(): Step( strategy_data=RandomStrategy(domain=domain2), condition=NumberOfExperimentsCondition(n_experiments=5), - max_parallelism=-1, ), Step( strategy_data=SoboStrategy( domain=benchmark.domain, acquisition_function=qNEI() ), condition=NumberOfExperimentsCondition(n_experiments=15), - max_parallelism=2, ), ], ) @@ -54,24 +53,22 @@ def test_StepwiseStrategy_invalid_AlwaysTrue(): Step( strategy_data=RandomStrategy(domain=benchmark.domain), condition=AlwaysTrueCondition(), - max_parallelism=-1, ), Step( strategy_data=SoboStrategy( domain=benchmark.domain, acquisition_function=qNEI() ), condition=NumberOfExperimentsCondition(n_experiments=10), - max_parallelism=2, ), ], ) @pytest.mark.parametrize( - "n_experiments, expected_strategy, expected_index", - [(5, RandomStrategy, 0), (10, SoboStrategy, 1)], + "n_experiments, expected_strategy", + [(5, strategies.RandomStrategy), (10, strategies.SoboStrategy)], ) -def test_StepWiseStrategy_get_step(n_experiments, expected_strategy, expected_index): +def test_StepWiseStrategy_get_step(n_experiments, expected_strategy): benchmark = Himmelblau() experiments = benchmark.f( benchmark.domain.inputs.sample(n_experiments), return_complete=True @@ -82,22 +79,20 @@ def test_StepWiseStrategy_get_step(n_experiments, expected_strategy, expected_in Step( strategy_data=RandomStrategy(domain=benchmark.domain), condition=NumberOfExperimentsCondition(n_experiments=6), - max_parallelism=-1, ), Step( strategy_data=SoboStrategy( domain=benchmark.domain, acquisition_function=qNEI() ), condition=NumberOfExperimentsCondition(n_experiments=10), - max_parallelism=2, ), ], ) - strategy = strategies.map(data_model) + strategy = cast(strategies.StepwiseStrategy, strategies.map(data_model)) strategy.tell(experiments) - i, step = strategy._get_step() - assert isinstance(step.strategy_data, expected_strategy) - assert i == expected_index + strategy, transform = strategy._get_step() + assert transform is None + assert isinstance(strategy, expected_strategy) def test_StepWiseStrategy_get_step_invalid(): @@ -109,51 +104,21 @@ def test_StepWiseStrategy_get_step_invalid(): Step( strategy_data=RandomStrategy(domain=benchmark.domain), condition=NumberOfExperimentsCondition(n_experiments=6), - max_parallelism=-1, ), Step( strategy_data=SoboStrategy( domain=benchmark.domain, acquisition_function=qNEI() ), condition=NumberOfExperimentsCondition(n_experiments=10), - max_parallelism=2, ), ], ) - strategy = strategies.map(data_model) + strategy = cast(strategies.StepwiseStrategy, strategies.map(data_model)) strategy.tell(experiments) with pytest.raises(ValueError, match="No condition could be satisfied."): strategy._get_step() -def test_StepWiseStrategy_invalid_ask(): - benchmark = Himmelblau() - data_model = StepwiseStrategy( - domain=benchmark.domain, - steps=[ - Step( - strategy_data=RandomStrategy(domain=benchmark.domain), - condition=NumberOfExperimentsCondition(n_experiments=8), - max_parallelism=2, - ), - Step( - strategy_data=SoboStrategy( - domain=benchmark.domain, acquisition_function=qNEI() - ), - condition=NumberOfExperimentsCondition(n_experiments=10), - max_parallelism=2, - ), - ], - ) - strategy = strategies.map(data_model) - experiments = benchmark.f(benchmark.domain.inputs.sample(2), return_complete=True) - strategy.tell(experiments=experiments) - with pytest.raises( - ValueError, match="Maximum number of candidates for step 0 is 2." - ): - strategy.ask(3) - - def test_StepWiseStrategy_ask(): benchmark = Himmelblau() experiments = benchmark.f(benchmark.domain.inputs.sample(2), return_complete=True) @@ -163,14 +128,12 @@ def test_StepWiseStrategy_ask(): Step( strategy_data=RandomStrategy(domain=benchmark.domain), condition=NumberOfExperimentsCondition(n_experiments=5), - max_parallelism=2, ), Step( strategy_data=SoboStrategy( domain=benchmark.domain, acquisition_function=qNEI() ), condition=NumberOfExperimentsCondition(n_experiments=10), - max_parallelism=2, ), ], ) diff --git a/tests/bofire/strategies/stepwise/test_transfroms.py b/tests/bofire/strategies/stepwise/test_transfroms.py new file mode 100644 index 000000000..4b036c69b --- /dev/null +++ b/tests/bofire/strategies/stepwise/test_transfroms.py @@ -0,0 +1,68 @@ +from typing import cast + +import numpy as np +import pandas as pd + +import bofire.strategies.api as strategies +from bofire.benchmarks.single import Himmelblau +from bofire.data_models.acquisition_functions.acquisition_function import qNEI +from bofire.data_models.strategies.predictives.sobo import SoboStrategy +from bofire.data_models.strategies.random import RandomStrategy +from bofire.data_models.strategies.stepwise.conditions import ( + AlwaysTrueCondition, + NumberOfExperimentsCondition, +) +from bofire.data_models.strategies.stepwise.stepwise import Step, StepwiseStrategy +from bofire.data_models.transforms.drop_data import DropDataTransform + + +def test_dropdata_transform(): + benchmark = Himmelblau() + d = benchmark.domain + params = d.inputs.get_keys() + d.outputs.get_keys() + + def test(to_be_removed_rows): + data_model = StepwiseStrategy( + domain=benchmark.domain, + steps=[ + Step( + strategy_data=RandomStrategy(domain=benchmark.domain), + condition=NumberOfExperimentsCondition(n_experiments=2), + ), + Step( + strategy_data=SoboStrategy( + domain=benchmark.domain, acquisition_function=qNEI() + ), + condition=AlwaysTrueCondition(), + transform=DropDataTransform( + to_be_removed_experiments=to_be_removed_rows + ), + ), + ], + ) + strategy = cast(strategies.StepwiseStrategy, strategies.map(data_model)) + n_samples = 6 + experiments = pd.concat( + [ + benchmark.domain.inputs.sample(n_samples), + pd.DataFrame({"y": [1] * n_samples}), + ], + axis=1, + ) + strategy.tell(experiments=experiments) + strategy.ask() + + last_strategy, _ = strategy._get_step() + assert last_strategy.experiments is not None and len( + last_strategy.experiments + ) == n_samples - len(to_be_removed_rows) + kept_rows = [i for i in range(n_samples) if i not in to_be_removed_rows] + for i, row in enumerate(kept_rows): + assert np.all( + last_strategy.experiments[params].values[i] + == experiments[params].values[row] + ) + + test([0]) + test([0, 1]) + test([1, 3])