From f47aa94d0717e4c03f11ea0ae78f63fd394d3661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20P=2E=20D=C3=BCrholt?= Date: Tue, 4 Feb 2025 11:40:13 +0100 Subject: [PATCH] add randomization for fractionalfactorial --- .../strategies/fractional_factorial.py | 4 ++ bofire/strategies/fractional_factorial.py | 16 +++++- tests/bofire/data_models/specs/strategies.py | 1 + .../strategies/test_fractional_factorial.py | 57 +++++++++++++++++++ 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/bofire/data_models/strategies/fractional_factorial.py b/bofire/data_models/strategies/fractional_factorial.py index 18b29bb45..32b9f5b4d 100644 --- a/bofire/data_models/strategies/fractional_factorial.py +++ b/bofire/data_models/strategies/fractional_factorial.py @@ -46,6 +46,10 @@ class FractionalFactorialStrategy(Strategy): int, Field(description="Number of reducing factors", ge=0), ] = 0 + randomize_runorder: bool = Field( + default=False, + description="If true, the run order is randomized, else it is deterministic.", + ) @classmethod def is_constraint_implemented(cls, my_type: Type[Constraint]) -> bool: diff --git a/bofire/strategies/fractional_factorial.py b/bofire/strategies/fractional_factorial.py index f3fc86f87..7c9e38342 100644 --- a/bofire/strategies/fractional_factorial.py +++ b/bofire/strategies/fractional_factorial.py @@ -25,6 +25,7 @@ def __init__( self.n_center = data_model.n_center self.n_generators = data_model.n_generators self.generator = data_model.generator + self.randomize_runoder = data_model.randomize_runorder def _get_continuous_design(self) -> pd.DataFrame: continuous_inputs = self.domain.inputs.get(ContinuousInput) @@ -69,23 +70,32 @@ def _ask(self, candidate_count: Optional[int] = None) -> pd.DataFrame: if len(self.domain.inputs.get(ContinuousInput)) > 0: design = self._get_continuous_design() if len(self.domain.inputs.get(ContinuousInput)) == len(self.domain.inputs): - return design + return self.randomize_design(design) categorical_design = self._get_categorical_design() if len(self.domain.inputs.get([CategoricalInput, DiscreteInput])) == len( self.domain.inputs ): - return categorical_design + return self.randomize_design(categorical_design) assert isinstance(design, pd.DataFrame) # combine the two designs - return pd.concat( + design = pd.concat( [ pd.concat([design] * len(categorical_design), ignore_index=True), pd.concat([categorical_design] * len(design), ignore_index=True), # type: ignore ], axis=1, ).sort_values(by=self.domain.inputs.get_keys([CategoricalInput, DiscreteInput])) + return self.randomize_design(design) + + def randomize_design(self, design: pd.DataFrame) -> pd.DataFrame: + """Randomize the run order of the design if `self.randomize_runorder` is True.""" + return ( + design.sample(frac=1, random_state=self._get_seed()).reset_index(drop=True) + if self.randomize_runoder + else design + ) def has_sufficient_experiments(self) -> bool: return True diff --git a/tests/bofire/data_models/specs/strategies.py b/tests/bofire/data_models/specs/strategies.py index 0aeab2699..cadbfbe5d 100644 --- a/tests/bofire/data_models/specs/strategies.py +++ b/tests/bofire/data_models/specs/strategies.py @@ -589,6 +589,7 @@ "n_center": 0, "n_generators": 0, "generator": "", + "randomize_runorder": False, }, ) diff --git a/tests/bofire/strategies/test_fractional_factorial.py b/tests/bofire/strategies/test_fractional_factorial.py index e5ce0bd0f..aa7438950 100644 --- a/tests/bofire/strategies/test_fractional_factorial.py +++ b/tests/bofire/strategies/test_fractional_factorial.py @@ -157,6 +157,63 @@ def test_FractionalFactorialStrategy_ask(): assert len(candidates) == 10 +def test_FractionalFactorialStrategy_randomize_runorder(): + # test no randomization + strategy_data = FractionalFactorialStrategy( + domain=Domain( + inputs=Inputs( + features=[ + ContinuousInput(key="a", bounds=(0, 1)), + ContinuousInput(key="b", bounds=(-2, 8)), + ], + ), + ), + randomize_runorder=False, + ) + strategy = strategies.map(strategy_data) + design = strategy.ask(None) + design2 = strategy.ask(None) + # test with randomization + assert_frame_equal(design, design2) + strategy_data = FractionalFactorialStrategy( + domain=Domain( + inputs=Inputs( + features=[ + ContinuousInput(key="a", bounds=(0, 1)), + ContinuousInput(key="b", bounds=(-2, 8)), + ], + ), + ), + randomize_runorder=True, + seed=42, + ) + strategy = strategies.map(strategy_data) + design = strategy.ask(None) + design2 = strategy.ask(None) + with pytest.raises(AssertionError): + assert_frame_equal(design, design2) + # test reproducibility with same seed for randomization + strategy_data = FractionalFactorialStrategy( + domain=Domain( + inputs=Inputs( + features=[ + ContinuousInput(key="a", bounds=(0, 1)), + ContinuousInput(key="b", bounds=(-2, 8)), + ], + ), + ), + randomize_runorder=True, + seed=42, + ) + strategy = strategies.map(strategy_data) + design3 = strategy.ask(None) + design4 = strategy.ask(None) + with pytest.raises(AssertionError): + assert_frame_equal(design3, design4) + assert_frame_equal(design, design3) + assert_frame_equal(design2, design4) + + def test_FractionalFactorialStrategy_ask_invalid(): strategy_data = FractionalFactorialStrategy( domain=Domain(