From e3fa24138d09ebb4ac20cfbfcf82e9fad655ad25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20P=2E=20D=C3=BCrholt?= Date: Mon, 24 Apr 2023 10:11:54 +0200 Subject: [PATCH] Efficient NChooseKs in BO (#156) --- bofire/benchmarks/single.py | 59 +- bofire/data_models/domain/domain.py | 43 +- .../strategies/predictives/botorch.py | 21 +- .../strategies/predictives/qehvi.py | 15 - .../strategies/predictives/qnehvi.py | 2 + .../strategies/predictives/qparego.py | 9 +- .../strategies/predictives/sobo.py | 19 - bofire/strategies/predictives/botorch.py | 320 ++++--- bofire/strategies/predictives/predictive.py | 53 +- bofire/strategies/predictives/qehvi.py | 8 +- bofire/strategies/predictives/qnehvi.py | 10 +- bofire/strategies/predictives/qparego.py | 151 +--- bofire/strategies/predictives/sobo.py | 9 +- bofire/strategies/random.py | 9 - bofire/strategies/samplers/sampler.py | 38 +- bofire/strategies/strategy.py | 35 +- bofire/utils/torch_tools.py | 77 +- requirements.txt | 2 +- tests/bofire/benchmarks/test_single.py | 16 +- tests/bofire/data_models/test_samplers.py | 13 + tests/bofire/strategies/test_ask.py | 4 +- tests/bofire/strategies/test_base.py | 137 ++- .../test_base_with_nchoosek_constraint.py | 825 ------------------ tests/bofire/strategies/test_qehvi.py | 41 +- tests/bofire/strategies/test_qparego.py | 31 +- tests/bofire/strategies/test_sobo.py | 37 +- tests/bofire/strategies/test_strategy.py | 37 +- tests/bofire/utils/test_torch_tools.py | 137 +++ .../005-Hartmann_with_nchoosek.ipynb | 182 ++++ tutorials/benchmarks/006-30dimBranin.ipynb | 147 ++++ 30 files changed, 1118 insertions(+), 1369 deletions(-) delete mode 100644 tests/bofire/strategies/test_base_with_nchoosek_constraint.py create mode 100644 tutorials/benchmarks/005-Hartmann_with_nchoosek.ipynb create mode 100644 tutorials/benchmarks/006-30dimBranin.ipynb diff --git a/bofire/benchmarks/single.py b/bofire/benchmarks/single.py index e47145a60..1117dc8f0 100644 --- a/bofire/benchmarks/single.py +++ b/bofire/benchmarks/single.py @@ -1,13 +1,16 @@ import math +from typing import Optional import numpy as np import pandas as pd import torch +from botorch.test_functions import Hartmann as botorch_hartmann from botorch.test_functions.synthetic import Branin as torchBranin from pydantic.types import PositiveInt from bofire.benchmarks.benchmark import Benchmark -from bofire.data_models.domain.api import Domain, Inputs, Outputs +from bofire.data_models.constraints.api import NChooseKConstraint +from bofire.data_models.domain.api import Constraints, Domain, Inputs, Outputs from bofire.data_models.features.api import ( CategoricalDescriptorInput, CategoricalInput, @@ -156,6 +159,60 @@ def get_optima(self) -> pd.DataFrame: ) +class Hartmann(Benchmark): + def __init__(self, dim: int = 6, allowed_k: Optional[int] = None) -> None: + super().__init__() + self._domain = Domain( + input_features=Inputs( + features=[ + ContinuousInput(key=f"x_{i}", bounds=(0, 1)) for i in range(dim) + ] + ), + output_features=Outputs( + features=[ContinuousOutput(key="y", objective=MinimizeObjective())] + ), + constraints=Constraints( + constraints=[ + NChooseKConstraint( + features=[f"x_{i}" for i in range(dim)], + min_count=0, + max_count=allowed_k, + none_also_valid=True, + ) + ] + ) + if allowed_k + else Constraints(), + ) + self._hartmann = botorch_hartmann(dim=dim) + + def get_optima(self) -> pd.DataFrame: + if self.dim != 6: + raise ValueError("Only available for dim==6.") + if len(self.domain.constraints) > 0: + raise ValueError("Not defined for NChooseK use case.") + return pd.DataFrame( + columns=[f"x_{i}" for i in range(self.dim)] + ["y"], + data=[[0.20169, 0.150011, 0.476874, 0.275332, 0.311652, 0.6573, -3.32237]], + ) + + @property + def dim(self) -> int: + return len(self.domain.inputs) + + def _f(self, candidates: pd.DataFrame) -> pd.DataFrame: + return pd.DataFrame( + { + "y": self._hartmann( + torch.from_numpy( + candidates[[f"x_{i}" for i in range(self.dim)]].values + ) + ), + "valid_y": [1 for _ in range(len(candidates))], + } + ) + + class Branin(Benchmark): def __init__(self) -> None: self._domain = Domain( diff --git a/bofire/data_models/domain/domain.py b/bofire/data_models/domain/domain.py index 1e1d5b74e..74da5dad0 100644 --- a/bofire/data_models/domain/domain.py +++ b/bofire/data_models/domain/domain.py @@ -597,7 +597,7 @@ def describe_experiments(self, experiments: pd.DataFrame) -> pd.DataFrame: ) def validate_candidates( - self, candidates: pd.DataFrame, only_inputs: bool = False, tol: float = 1e-6 + self, candidates: pd.DataFrame, only_inputs: bool = False, tol: float = 1e-5 ) -> pd.DataFrame: """Method to check the validty of porposed candidates @@ -622,26 +622,41 @@ def validate_candidates( self.input_features.validate_inputs(candidates) # check if all constraints are fulfilled if not self.cnstrs.is_fulfilled(candidates, tol=tol).all(): - raise ValueError("Constraints not fulfilled.") + raise ValueError(f"Constraints not fulfilled: {candidates}") # for each continuous output feature with an attached objective object if not only_inputs: assert isinstance(self.output_features, Outputs) - for key in self.output_features.get_keys_by_objective(Objective): - # check that pred, sd, and des cols are specified and numerical - for col in [f"{key}_pred", f"{key}_sd", f"{key}_des"]: - if col not in candidates: - raise ValueError(f"missing column {col}") - if (not is_numeric(candidates[col])) and ( - not candidates[col].isnull().to_numpy().all() - ): - raise ValueError( - f"not all values of output feature `{key}` are numerical" + + cols = list( + itertools.chain.from_iterable( + [ + [f"{key}_pred", f"{key}_sd", f"{key}_des"] + for key in self.output_features.get_keys_by_objective(Objective) + ] + + [ + [f"{key}_pred", f"{key}_sd"] + for key in self.output_features.get_keys_by_objective( + excludes=Objective, includes=None # type: ignore ) + ] + ) + ) + + # check that pred, sd, and des cols are specified and numerical + for col in cols: + if col not in candidates: + raise ValueError(f"missing column {col}") + if (not is_numeric(candidates[col])) and ( + not candidates[col].isnull().to_numpy().all() + ): + raise ValueError(f"not all values of column `{col}` are numerical") + # validate no additional cols exist if_count = len(self.get_features(Input)) - of_count = len(self.output_features.get_keys_by_objective(Objective)) + of_count = len(self.outputs.get_by_objective(includes=Objective)) + of_count_w = len(self.outputs.get_by_objective(excludes=Objective, includes=None)) # type: ignore # input features, prediction, standard deviation and reward for each output feature, 3 additional usefull infos: reward, aquisition function, strategy - if len(candidates.columns) != if_count + 3 * of_count: + if len(candidates.columns) != if_count + 3 * of_count + 2 * of_count_w: raise ValueError("additional columns found") return candidates diff --git a/bofire/data_models/strategies/predictives/botorch.py b/bofire/data_models/strategies/predictives/botorch.py index a5bba8995..7b9dfb2a7 100644 --- a/bofire/data_models/strategies/predictives/botorch.py +++ b/bofire/data_models/strategies/predictives/botorch.py @@ -1,7 +1,12 @@ -from typing import Optional +from typing import Optional, Type from pydantic import PositiveInt, root_validator, validator +from bofire.data_models.constraints.api import ( + Constraint, + 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 @@ -26,6 +31,20 @@ class BotorchStrategy(PredictiveStrategy): discrete_method: CategoricalMethodEnum = CategoricalMethodEnum.EXHAUSTIVE surrogate_specs: Optional[BotorchSurrogates] = None + @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]: + return False + return True + @validator("num_sobol_samples") def validate_num_sobol_samples(cls, v): if is_power_of_two(v) is False: diff --git a/bofire/data_models/strategies/predictives/qehvi.py b/bofire/data_models/strategies/predictives/qehvi.py index 399314b72..17a991541 100644 --- a/bofire/data_models/strategies/predictives/qehvi.py +++ b/bofire/data_models/strategies/predictives/qehvi.py @@ -2,7 +2,6 @@ from pydantic import validator -from bofire.data_models.constraints.api import Constraint, NChooseKConstraint from bofire.data_models.features.api import Feature from bofire.data_models.objectives.api import ( MaximizeObjective, @@ -33,20 +32,6 @@ def validate_ref_point(cls, v, values): ) return v - @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 == NChooseKConstraint: - return False - return True - @classmethod def is_feature_implemented(cls, my_type: Type[Feature]) -> bool: """Method to check if a specific feature type is implemented for the strategy diff --git a/bofire/data_models/strategies/predictives/qnehvi.py b/bofire/data_models/strategies/predictives/qnehvi.py index b684e7e38..5a0d8a9f7 100644 --- a/bofire/data_models/strategies/predictives/qnehvi.py +++ b/bofire/data_models/strategies/predictives/qnehvi.py @@ -3,6 +3,7 @@ from pydantic import confloat from bofire.data_models.objectives.api import ( + CloseToTargetObjective, MaximizeObjective, MaximizeSigmoidObjective, MinimizeObjective, @@ -33,4 +34,5 @@ def is_objective_implemented(cls, my_type: Type[Objective]) -> bool: MinimizeSigmoidObjective, MaximizeSigmoidObjective, TargetObjective, + CloseToTargetObjective, ] diff --git a/bofire/data_models/strategies/predictives/qparego.py b/bofire/data_models/strategies/predictives/qparego.py index d0c0e8930..1dd1d03af 100644 --- a/bofire/data_models/strategies/predictives/qparego.py +++ b/bofire/data_models/strategies/predictives/qparego.py @@ -1,8 +1,8 @@ from typing import Literal, Type -from bofire.data_models.constraints.api import Constraint, NChooseKConstraint from bofire.data_models.features.api import Feature from bofire.data_models.objectives.api import ( + CloseToTargetObjective, MaximizeObjective, MaximizeSigmoidObjective, MinimizeObjective, @@ -18,12 +18,6 @@ class QparegoStrategy(MultiobjectiveStrategy): type: Literal["QparegoStrategy"] = "QparegoStrategy" - @classmethod - def is_constraint_implemented(cls, my_type: Type[Constraint]) -> bool: - if my_type == NChooseKConstraint: - return False - return True - @classmethod def is_feature_implemented(cls, my_type: Type[Feature]) -> bool: return True @@ -36,6 +30,7 @@ def is_objective_implemented(cls, my_type: Type[Objective]) -> bool: TargetObjective, MinimizeSigmoidObjective, MaximizeSigmoidObjective, + CloseToTargetObjective, ]: return False return True diff --git a/bofire/data_models/strategies/predictives/sobo.py b/bofire/data_models/strategies/predictives/sobo.py index 4abfccd0e..2ea3fbe69 100644 --- a/bofire/data_models/strategies/predictives/sobo.py +++ b/bofire/data_models/strategies/predictives/sobo.py @@ -3,11 +3,6 @@ from pydantic import validator from bofire.data_models.acquisition_functions.api import AnyAcquisitionFunction -from bofire.data_models.constraints.api import ( - Constraint, - NonlinearEqualityConstraint, - NonlinearInequalityConstraint, -) from bofire.data_models.features.api import Feature from bofire.data_models.objectives.api import BotorchConstrainedObjective, Objective from bofire.data_models.strategies.predictives.botorch import BotorchStrategy @@ -16,20 +11,6 @@ class SoboBaseStrategy(BotorchStrategy): acquisition_function: AnyAcquisitionFunction - @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]: - return False - return True - @classmethod def is_feature_implemented(cls, my_type: Type[Feature]) -> bool: """Method to check if a specific feature type is implemented for the strategy diff --git a/bofire/strategies/predictives/botorch.py b/bofire/strategies/predictives/botorch.py index 7a75a2e5e..d8da33c70 100644 --- a/bofire/strategies/predictives/botorch.py +++ b/bofire/strategies/predictives/botorch.py @@ -1,6 +1,6 @@ import copy from abc import abstractmethod -from typing import Callable, Dict, Optional, Tuple +from typing import Callable, Dict, List, Optional, Tuple import numpy as np import pandas as pd @@ -8,8 +8,12 @@ from botorch.acquisition.acquisition import AcquisitionFunction from botorch.acquisition.utils import get_infeasible_cost from botorch.models.gpytorch import GPyTorchModel -from botorch.optim.optimize import optimize_acqf, optimize_acqf_mixed -from pydantic.types import NonNegativeInt +from botorch.optim.initializers import gen_batch_initial_conditions +from botorch.optim.optimize import ( + optimize_acqf, + optimize_acqf_list, + optimize_acqf_mixed, +) from torch import Tensor from bofire.data_models.constraints.api import ( @@ -32,7 +36,12 @@ from bofire.strategies.predictives.predictive import PredictiveStrategy from bofire.strategies.samplers.polytope import PolytopeSampler from bofire.surrogates.botorch_surrogates import BotorchSurrogates -from bofire.utils.torch_tools import get_linear_constraints, tkwargs +from bofire.utils.torch_tools import ( + get_initial_conditions_generator, + get_linear_constraints, + get_nchoosek_constraints, + tkwargs, +) def is_power_of_two(n): @@ -55,7 +64,6 @@ def __init__( self.surrogate_specs = BotorchSurrogates(data_model=data_model.surrogate_specs) # type: ignore torch.manual_seed(self.seed) - acqf: Optional[AcquisitionFunction] = None model: Optional[GPyTorchModel] = None @property @@ -104,7 +112,6 @@ def _predict(self, transformed: pd.DataFrame) -> Tuple[np.ndarray, np.ndarray]: raise ValueError("Wrong dimension of posterior mean. Expecting 2 or 3.") return preds, stds - # TODO: test this def calc_acquisition( self, candidates: pd.DataFrame, combined: bool = False ) -> np.ndarray: @@ -118,68 +125,49 @@ def calc_acquisition( Returns: np.ndarray: Dataframe with the acquisition values. """ + acqf = self._get_acqfs(1)[0] + transformed = self.domain.inputs.transform( candidates, self.input_preprocessing_specs ) X = torch.from_numpy(transformed.values).to(**tkwargs) if combined is False: X = X.unsqueeze(-2) - return self.acqf.forward(X).cpu().detach().numpy() # type: ignore - - # TODO: test this - def _choose_from_pool( - self, - candidate_pool: pd.DataFrame, - candidate_count: Optional[NonNegativeInt] = None, - ) -> pd.DataFrame: - """Method to choose a set of candidates from a candidate pool. - - Args: - candidate_pool (pd.DataFrame): The pool of candidates from which the candidates should be chosen. - candidate_count (Optional[NonNegativeInt], optional): Number of candidates to choose. Defaults to None. - Returns: - pd.DataFrame: The chosen set of candidates. - """ - - acqf_values = self.calc_acquisition(candidate_pool) - - return candidate_pool.iloc[ - np.argpartition(acqf_values, -1 * candidate_count)[-candidate_count:] # type: ignore - ] - - def _ask(self, candidate_count: int) -> pd.DataFrame: - """[summary] - - Args: - candidate_count (int, optional): [description]. Defaults to 1. - - Returns: - pd.DataFrame: [description] - """ + with torch.no_grad(): + vals = acqf.forward(X).cpu().detach().numpy() - assert candidate_count > 0, "candidate_count has to be larger than zero." + return vals - # optimize - # we have to distuinguish the following scenarios - # - no categoricals - check - # - categoricals with one hot and free variables - # - categoricals with one hot and exhaustive screening, could be in combination with garrido merchan - check - # - categoricals with one hot and OEN, could be in combination with garrido merchan - OEN not implemented - # - descriptized categoricals not yet implemented + def _setup_ask(self): + """Generates argument that can by passed to one of botorch's `optimize_acqf` method.""" num_categorical_features = len( self.domain.get_features([CategoricalInput, DiscreteInput]) ) num_categorical_combinations = len( self.domain.inputs.get_categorical_combinations() ) - assert self.acqf is not None - lower, upper = self.domain.inputs.get_bounds( specs=self.input_preprocessing_specs ) bounds = torch.tensor([lower, upper]).to(**tkwargs) - + # setup nchooseks + if len(self.domain.cnstrs.get(NChooseKConstraint)) == 0: + ic_generator = None + ic_gen_kwargs = {} + nchooseks = None + else: + ic_generator = gen_batch_initial_conditions + ic_gen_kwargs = { + "generator": get_initial_conditions_generator( + strategy=PolytopeSampler( + data_model=PolytopeSamplerDataModel(domain=self.domain), + ), + transform_specs=self.input_preprocessing_specs, + ) + } + nchooseks = get_nchoosek_constraints(self.domain) + # setup fixed features if ( (num_categorical_features == 0) or (num_categorical_combinations == 1) @@ -193,68 +181,30 @@ def _ask(self, candidate_count: int) -> pd.DataFrame: ] ) ) - ) and len(self.domain.cnstrs.get(NChooseKConstraint)) == 0: - candidates = optimize_acqf( - acq_function=self.acqf, - bounds=bounds, - q=candidate_count, - num_restarts=self.num_restarts, - raw_samples=self.num_raw_samples, - equality_constraints=get_linear_constraints( - domain=self.domain, constraint=LinearEqualityConstraint # type: ignore - ), - inequality_constraints=get_linear_constraints( - domain=self.domain, constraint=LinearInequalityConstraint # type: ignore - ), - fixed_features=self.get_fixed_features(), - return_best_only=True, - ) - - elif ( - CategoricalMethodEnum.EXHAUSTIVE - in [self.categorical_method, self.descriptor_method, self.discrete_method] - ) and len(self.domain.cnstrs.get(NChooseKConstraint)) == 0: - candidates = optimize_acqf_mixed( - acq_function=self.acqf, - bounds=bounds, - q=candidate_count, - num_restarts=self.num_restarts, - raw_samples=self.num_raw_samples, - equality_constraints=get_linear_constraints( - domain=self.domain, constraint=LinearEqualityConstraint # type: ignore - ), - inequality_constraints=get_linear_constraints( - domain=self.domain, constraint=LinearInequalityConstraint # type: ignore - ), - fixed_features_list=self.get_categorical_combinations(), - ) - - elif len(self.domain.cnstrs.get(NChooseKConstraint)) > 0: - candidates = optimize_acqf_mixed( - acq_function=self.acqf, - bounds=bounds, - q=candidate_count, - num_restarts=self.num_restarts, - raw_samples=self.num_raw_samples, - equality_constraints=get_linear_constraints( - domain=self.domain, constraint=LinearEqualityConstraint # type: ignore - ), - inequality_constraints=get_linear_constraints( - domain=self.domain, constraint=LinearInequalityConstraint # type: ignore - ), - fixed_features_list=self.get_fixed_values_list(), - ) - + ): + fixed_features = self.get_fixed_features() + fixed_features_list = None else: - raise IOError() + fixed_features = None + fixed_features_list = self.get_categorical_combinations() + return ( + bounds, + ic_generator, + ic_gen_kwargs, + nchooseks, + fixed_features, + fixed_features_list, + ) + + def _postprocess_candidates(self, candidates: Tensor) -> pd.DataFrame: + """Converts a tensor of candidates to a pandas Dataframe. - # postprocess the results - # TODO: in case of free we have to transform back the candidates first and then compute the metrics - # otherwise the prediction holds only for the infeasible solution, this solution should then also be - # applicable for >1d descriptors - # preds = self.model.posterior(X=candidates[0]).mean.detach().numpy() # type: ignore - # stds = np.sqrt(self.model.posterior(X=candidates[0]).variance.detach().numpy()) # type: ignore + Args: + candidates (Tensor): Tensor of candidates returned from `optimize_acqf`. + Returns: + pd.DataFrame: Dataframe with candidates. + """ input_feature_keys = [ item for key in self.domain.inputs.get_keys() @@ -262,33 +212,103 @@ def _ask(self, candidate_count: int) -> pd.DataFrame: ] df_candidates = pd.DataFrame( - data=candidates[0].detach().numpy(), columns=input_feature_keys + data=candidates.detach().numpy(), columns=input_feature_keys ) - preds, stds = self._predict(df_candidates) - df_candidates = self.domain.inputs.inverse_transform( df_candidates, self.input_preprocessing_specs ) - for i, feat in enumerate(self.domain.outputs.get_by_objective(excludes=None)): - df_candidates[feat.key + "_pred"] = preds[:, i] - df_candidates[feat.key + "_sd"] = stds[:, i] - df_candidates[feat.key + "_des"] = feat.objective(preds[:, i]) # type: ignore + preds = self.predict(df_candidates) + return pd.concat((df_candidates, preds), axis=1) - return df_candidates + def _ask(self, candidate_count: int) -> pd.DataFrame: + """[summary] - def _tell(self) -> None: - self.init_acqf() + Args: + candidate_count (int, optional): [description]. Defaults to 1. + + Returns: + pd.DataFrame: [description] + """ + + assert candidate_count > 0, "candidate_count has to be larger than zero." + + ( + bounds, + ic_generator, + ic_gen_kwargs, + nchooseks, + fixed_features, + fixed_features_list, + ) = self._setup_ask() + + acqfs = self._get_acqfs(candidate_count) + + if len(acqfs) > 1: + candidates, _ = optimize_acqf_list( + acq_function_list=acqfs, + bounds=bounds, + num_restarts=self.num_restarts, + raw_samples=self.num_raw_samples, + equality_constraints=get_linear_constraints( + domain=self.domain, constraint=LinearEqualityConstraint # type: ignore + ), + inequality_constraints=get_linear_constraints( + domain=self.domain, constraint=LinearInequalityConstraint # type: ignore + ), + nonlinear_inequality_constraints=nchooseks, + fixed_features=fixed_features, + fixed_features_list=fixed_features_list, + ic_gen_kwargs=ic_gen_kwargs, + ic_generator=ic_generator, + options={"batch_limit": 5, "maxiter": 200}, + ) + else: + if fixed_features_list: + candidates, _ = optimize_acqf_mixed( + acq_function=acqfs[0], + bounds=bounds, + q=candidate_count, + num_restarts=self.num_restarts, + raw_samples=self.num_raw_samples, + equality_constraints=get_linear_constraints( + domain=self.domain, constraint=LinearEqualityConstraint # type: ignore + ), + inequality_constraints=get_linear_constraints( + domain=self.domain, constraint=LinearInequalityConstraint # type: ignore + ), + nonlinear_inequality_constraints=nchooseks, + fixed_features_list=fixed_features_list, + ic_generator=ic_generator, + ic_gen_kwargs=ic_gen_kwargs, + ) + else: + candidates, _ = optimize_acqf( + acq_function=acqfs[0], + bounds=bounds, + q=candidate_count, + num_restarts=self.num_restarts, + raw_samples=self.num_raw_samples, + equality_constraints=get_linear_constraints( + domain=self.domain, constraint=LinearEqualityConstraint # type: ignore + ), + inequality_constraints=get_linear_constraints( + domain=self.domain, constraint=LinearInequalityConstraint # type: ignore + ), + fixed_features=fixed_features, + nonlinear_inequality_constraints=nchooseks, + return_best_only=True, + ic_generator=ic_generator, + **ic_gen_kwargs, + ) + return self._postprocess_candidates(candidates=candidates) - def init_acqf(self) -> None: - self._init_acqf() - return + def _tell(self) -> None: + pass @abstractmethod - def _init_acqf( - self, - ) -> None: + def _get_acqfs(self, n: int) -> List[AcquisitionFunction]: pass def get_fixed_features(self): @@ -426,53 +446,19 @@ def get_categorical_combinations(self): list_of_fixed_features.append(fixed_features) return list_of_fixed_features - def get_nchoosek_combinations(self): - """ - generate a list of fixed values dictionaries from n-choose-k constraints - """ - - # generate botorch-friendly fixed values - features2idx = self._features2idx - used_features, unused_features = self.domain.get_nchoosek_combinations( - exhaustive=True - ) - fixed_values_list_cc = [] - for used, unused in zip(used_features, unused_features): - fixed_values = {} - - # sets unused features to zero - for f_key in unused: - fixed_values[features2idx[f_key][0]] = 0.0 - - fixed_values_list_cc.append(fixed_values) - - if len(fixed_values_list_cc) == 0: - fixed_values_list_cc.append({}) # any better alternative here? - - return fixed_values_list_cc - - def get_fixed_values_list(self): - # CARTESIAN PRODUCTS: fixed values from categorical combinations X fixed values from nchoosek constraints - fixed_values_full = [] - - for ff1 in self.get_categorical_combinations(): - for ff2 in self.get_nchoosek_combinations(): - ff = ff1.copy() - ff.update(ff2) - fixed_values_full.append(ff) - - return fixed_values_full - def has_sufficient_experiments( self, ) -> bool: if self.experiments is None: return False - degrees_of_freedom = len(self.domain.get_features(Input)) - len( - self.get_fixed_features() - ) - # degrees_of_freedom = len(self.domain.get_features(Input)) + 1 - if self.experiments.shape[0] > degrees_of_freedom + 1: + if ( + len( + self.domain.outputs.preprocess_experiments_all_valid_outputs( + experiments=self.experiments + ) + ) + > 1 + ): return True return False @@ -511,7 +497,7 @@ def get_infeasible_cost( data_model=PolytopeSamplerDataModel(domain=self.domain) ) samples = torch.from_numpy( - sampler.ask(n=n_samples, return_all=False).values + sampler.ask(candidate_count=n_samples, return_all=False).values ).to(**tkwargs) X = ( torch.cat((X_train, X_pending, samples)) diff --git a/bofire/strategies/predictives/predictive.py b/bofire/strategies/predictives/predictive.py index 48ecb14b7..4ded7bb47 100644 --- a/bofire/strategies/predictives/predictive.py +++ b/bofire/strategies/predictives/predictive.py @@ -6,7 +6,6 @@ from pydantic import PositiveInt from bofire.data_models.features.api import TInputTransformSpecs -from bofire.data_models.objectives.api import Objective from bofire.data_models.strategies.api import Strategy as DataModel from bofire.strategies.data_models.candidate import Candidate from bofire.strategies.data_models.values import InputValue, OutputValue @@ -36,14 +35,12 @@ def ask( self, candidate_count: Optional[PositiveInt] = None, add_pending: bool = False, - candidate_pool: Optional[pd.DataFrame] = None, ) -> pd.DataFrame: """Function to generate new candidates. Args: candidate_count (PositiveInt, optional): Number of candidates to be generated. If not provided, the number of candidates is determined automatically. Defaults to None. add_pending (bool, optional): If true the proposed candidates are added to the set of pending experiments. Defaults to False. - candidate_pool (pd.DataFrame, optional): Pool of candidates from which a final set of candidates should be chosen. If not provided, pool independent candidates are provided. Defaults to None. Returns: pd.DataFrame: DataFrame with candidates (proposed experiments) @@ -51,13 +48,7 @@ def ask( candidates = super().ask( candidate_count=candidate_count, add_pending=add_pending, - candidate_pool=candidate_pool, ) - # we have to generate predictions for the candidate pool candidates - if candidate_pool is not None: - pred = self.predict(candidates) - pred.index = candidates.index - candidates = pd.concat([candidates, pred], axis=1) self.domain.validate_candidates(candidates=candidates) return candidates @@ -105,22 +96,13 @@ def predict(self, experiments: pd.DataFrame) -> pd.DataFrame: if stds is not None: predictions = pd.DataFrame( data=np.hstack((preds, stds)), - columns=[ - "%s_pred" % feat.key - for feat in self.domain.outputs.get_by_objective(Objective) - ] - + [ - "%s_sd" % feat.key - for feat in self.domain.outputs.get_by_objective(Objective) - ], + columns=["%s_pred" % feat.key for feat in self.domain.outputs.get()] + + ["%s_sd" % feat.key for feat in self.domain.outputs.get()], ) else: predictions = pd.DataFrame( data=preds, - columns=[ - "%s_pred" % feat.key - for feat in self.domain.outputs.get_by_objective(Objective) - ], + columns=["%s_pred" % feat.key for feat in self.domain.outputs.get()], ) desis = self.domain.outputs(predictions, predictions=True) predictions = pd.concat((predictions, desis), axis=1) @@ -163,30 +145,15 @@ def to_candidates(self, candidates: pd.DataFrame) -> List[Candidate]: for key in self.domain.inputs.get_keys() }, outputValues={ - key: OutputValue( - predictedValue=row[f"{key}_pred"], - standardDeviation=row[f"{key}_sd"], - objective=row[f"{key}_des"], + feat.key: OutputValue( + predictedValue=row[f"{feat.key}_pred"], + standardDeviation=row[f"{feat.key}_sd"], + objective=row[f"{feat.key}_des"] + if feat.objective is not None # type: ignore + else 1.0, ) - for key in self.domain.outputs.get_keys() + for feat in self.domain.outputs.get() }, ) for _, row in candidates.iterrows() ] - - @abstractmethod - def _choose_from_pool( - self, - candidate_pool: pd.DataFrame, - candidate_count: Optional[PositiveInt] = None, - ) -> pd.DataFrame: - """Abstract method to implement how a strategy chooses a set of candidates from a candidate pool. - - Args: - candidate_pool (pd.DataFrame): The pool of candidates from which the candidates should be chosen. - candidate_count (Optional[PositiveInt], optional): Number of candidates to choose. Defaults to None. - - Returns: - pd.DataFrame: The chosen set of candidates. - """ - pass diff --git a/bofire/strategies/predictives/qehvi.py b/bofire/strategies/predictives/qehvi.py index 86816c3ea..8a9075218 100644 --- a/bofire/strategies/predictives/qehvi.py +++ b/bofire/strategies/predictives/qehvi.py @@ -33,7 +33,7 @@ def __init__( ref_point: Optional[dict] = None objective: Optional[MCMultiOutputObjective] = None - def _init_acqf(self) -> None: + def _get_acqfs(self, n) -> List[qExpectedHypervolumeImprovement]: df = self.domain.outputs.preprocess_experiments_all_valid_outputs( self.experiments ) @@ -62,7 +62,7 @@ def _init_acqf(self) -> None: assert self.model is not None # setup the acqf - self.acqf = qExpectedHypervolumeImprovement( + acqf = qExpectedHypervolumeImprovement( model=self.model, ref_point=ref_point, # use known reference point partitioning=partitioning, @@ -71,8 +71,8 @@ def _init_acqf(self) -> None: objective=self._get_objective(), X_pending=X_pending, ) - self.acqf._default_sample_shape = torch.Size([self.num_sobol_samples]) - return + acqf._default_sample_shape = torch.Size([self.num_sobol_samples]) + return [acqf] def _get_objective(self) -> GenericMCMultiOutputObjective: objective = get_multiobjective_objective(output_features=self.domain.outputs) diff --git a/bofire/strategies/predictives/qnehvi.py b/bofire/strategies/predictives/qnehvi.py index 7326699c1..a18e18078 100644 --- a/bofire/strategies/predictives/qnehvi.py +++ b/bofire/strategies/predictives/qnehvi.py @@ -1,3 +1,5 @@ +from typing import List + import torch from botorch.acquisition.multi_objective.monte_carlo import ( qNoisyExpectedHypervolumeImprovement, @@ -18,7 +20,7 @@ def __init__( super().__init__(data_model=data_model, **kwargs) self.alpha = data_model.alpha - def _init_acqf(self) -> None: + def _get_acqfs(self, n) -> List[qNoisyExpectedHypervolumeImprovement]: X_train, X_pending = self.get_acqf_input_tensors() # get etas and constraints @@ -30,7 +32,7 @@ def _init_acqf(self) -> None: assert self.model is not None # if the reference point is not defined it has to be calculated from data - self.acqf = qNoisyExpectedHypervolumeImprovement( + acqf = qNoisyExpectedHypervolumeImprovement( model=self.model, ref_point=self.get_adjusted_refpoint(), X_baseline=X_train, @@ -43,5 +45,5 @@ def _init_acqf(self) -> None: eta=etas, alpha=self.alpha, ) - self.acqf._default_sample_shape = torch.Size([self.num_sobol_samples]) - return + acqf._default_sample_shape = torch.Size([self.num_sobol_samples]) + return [acqf] diff --git a/bofire/strategies/predictives/qparego.py b/bofire/strategies/predictives/qparego.py index ef76ea3ac..d04b16df3 100644 --- a/bofire/strategies/predictives/qparego.py +++ b/bofire/strategies/predictives/qparego.py @@ -1,21 +1,12 @@ -from typing import Union +from typing import List, Union -import numpy as np -import pandas as pd import torch +from botorch.acquisition.acquisition import AcquisitionFunction from botorch.acquisition.objective import ConstrainedMCObjective, GenericMCObjective from botorch.acquisition.utils import get_acquisition_function -from botorch.optim.optimize import optimize_acqf_list from botorch.utils.multi_objective.scalarization import get_chebyshev_scalarization from botorch.utils.sampling import sample_simplex -from bofire.data_models.constraints.api import ( - LinearEqualityConstraint, - LinearInequalityConstraint, - NChooseKConstraint, -) -from bofire.data_models.enum import CategoricalMethodEnum -from bofire.data_models.features.api import CategoricalInput from bofire.data_models.objectives.api import ( CloseToTargetObjective, MaximizeObjective, @@ -25,7 +16,6 @@ from bofire.strategies.predictives.botorch import BotorchStrategy from bofire.utils.multiobjective import get_ref_point_mask from bofire.utils.torch_tools import ( - get_linear_constraints, get_multiobjective_objective, get_output_constraints, tkwargs, @@ -43,21 +33,11 @@ def __init__( ): super().__init__(data_model=data_model, **kwargs) - def _init_acqf(self) -> None: - pass - - def calc_acquisition(self, experiments: pd.DataFrame, combined: bool = False): - raise ValueError("ACQF calc not implemented for qparego") - - def get_objective( - self, pred: torch.Tensor + def _get_objective( + self, ) -> Union[GenericMCObjective, ConstrainedMCObjective]: """Returns the scalarized objective. - Args: - pred (torch.Tensor): Predictions for the training data from the - trained model. - Returns: Union[GenericMCObjective, ConstrainedMCObjective]: the botorch objective. """ @@ -82,8 +62,18 @@ def get_objective( obj_callable = get_multiobjective_objective(output_features=self.domain.outputs) + df_preds = self.predict( + self.domain.outputs.preprocess_experiments_any_valid_output( + experiments=self.experiments + ) + ) + + preds = torch.from_numpy( + df_preds[[f"{key}_pred" for key in self.domain.outputs.get_keys()]].values + ).to(**tkwargs) + scalarization = get_chebyshev_scalarization( - weights=weights, Y=obj_callable(pred, None) * ref_point_mask + weights=weights, Y=obj_callable(preds, None) * ref_point_mask ) def objective(Z, X=None): @@ -99,113 +89,24 @@ def objective(Z, X=None): ) return GenericMCObjective(scalarization) - def _ask(self, candidate_count: int): - assert candidate_count > 0, "candidate_count has to be larger than zero." - - acqf_list = [] - with torch.no_grad(): - clean_experiments = self.domain.outputs.preprocess_experiments_any_valid_output( - self.experiments # type: ignore - ) - transformed = self.domain.inputs.transform( - clean_experiments, self.input_preprocessing_specs - ) + def _get_acqfs(self, n: int) -> List[AcquisitionFunction]: + assert self.is_fitted is True, "Model not trained." - train_x = torch.from_numpy(transformed.values).to(**tkwargs) + acqfs = [] - pred = self.model.posterior(train_x).mean # type: ignore - - clean_experiments = self.domain.outputs.preprocess_experiments_all_valid_outputs( - self.experiments # type: ignore - ) - transformed = self.domain.inputs.transform( - clean_experiments, self.input_preprocessing_specs - ) - observed_x = torch.from_numpy(transformed.values).to(**tkwargs) + X_train, X_pending = self.get_acqf_input_tensors() - # TODO: unite it with SOBO and also add the other acquisition functions - for i in range(candidate_count): - assert self.model is not None + assert self.model is not None + for i in range(n): acqf = get_acquisition_function( acquisition_function_name="qNEI", model=self.model, - objective=self.get_objective(pred), - X_observed=observed_x, + objective=self._get_objective(), + X_observed=X_train, + X_pending=X_pending if i == 0 else None, mc_samples=self.num_sobol_samples, qmc=True, prune_baseline=True, ) - acqf_list.append(acqf) - - # optimize - lower, upper = self.domain.inputs.get_bounds(self.input_preprocessing_specs) - - num_categorical_features = len(self.domain.get_features(CategoricalInput)) - num_categorical_combinations = len( - self.domain.inputs.get_categorical_combinations() - ) - - fixed_features = None - fixed_features_list = None - - if ( - (num_categorical_features == 0) - or (num_categorical_combinations == 1) - or ( - (self.categorical_method == CategoricalMethodEnum.FREE) - and (self.descriptor_method == CategoricalMethodEnum.FREE) - ) - ) and len(self.domain.cnstrs.get(NChooseKConstraint)) == 0: - fixed_features = self.get_fixed_features() - - elif ( - (self.categorical_method == CategoricalMethodEnum.EXHAUSTIVE) - or (self.descriptor_method == CategoricalMethodEnum.EXHAUSTIVE) - ) and len(self.domain.cnstrs.get(NChooseKConstraint)) == 0: - fixed_features_list = self.get_categorical_combinations() - - elif len(self.domain.cnstrs.get(NChooseKConstraint)) > 0: - fixed_features_list = self.get_fixed_values_list() - else: - raise IOError() - - candidates, _ = optimize_acqf_list( - acq_function_list=acqf_list, - bounds=torch.tensor([lower, upper]).to(**tkwargs), - num_restarts=self.num_restarts, - raw_samples=self.num_raw_samples, - equality_constraints=get_linear_constraints( - domain=self.domain, constraint=LinearEqualityConstraint # type: ignore - ), - inequality_constraints=get_linear_constraints( - domain=self.domain, constraint=LinearInequalityConstraint # type: ignore - ), - fixed_features=fixed_features, - fixed_features_list=fixed_features_list, - options={"batch_limit": 5, "maxiter": 200}, - ) - - preds = self.model.posterior(X=candidates).mean.detach().numpy() # type: ignore - stds = np.sqrt(self.model.posterior(X=candidates).variance.detach().numpy()) # type: ignore - - input_feature_keys = [ - item - for key in self.domain.inputs.get_keys() - for item in self._features2names[key] - ] - - df_candidates = pd.DataFrame( - data=candidates.detach().numpy(), - columns=input_feature_keys, - ) - - df_candidates = self.domain.inputs.inverse_transform( - df_candidates, self.input_preprocessing_specs - ) - - for i, feat in enumerate(self.domain.outputs.get_by_objective(excludes=None)): - df_candidates[feat.key + "_pred"] = preds[:, i] - df_candidates[feat.key + "_sd"] = stds[:, i] - df_candidates[feat.key + "_des"] = feat.objective(preds[:, i]) # type: ignore - - return df_candidates + acqfs.append(acqf) + return acqfs diff --git a/bofire/strategies/predictives/sobo.py b/bofire/strategies/predictives/sobo.py index eec8d43cb..9a08d9b4a 100644 --- a/bofire/strategies/predictives/sobo.py +++ b/bofire/strategies/predictives/sobo.py @@ -1,7 +1,8 @@ -from typing import Union +from typing import List, Union import torch from botorch.acquisition import get_acquisition_function +from botorch.acquisition.acquisition import AcquisitionFunction from botorch.acquisition.objective import ConstrainedMCObjective, GenericMCObjective from botorch.models.gpytorch import GPyTorchModel @@ -31,12 +32,12 @@ def __init__( super().__init__(data_model=data_model, **kwargs) self.acquisition_function = data_model.acquisition_function - def _init_acqf(self) -> None: + def _get_acqfs(self, n) -> List[AcquisitionFunction]: assert self.is_fitted is True, "Model not trained." X_train, X_pending = self.get_acqf_input_tensors() - self.acqf = get_acquisition_function( + acqf = get_acquisition_function( self.acquisition_function.__class__.__name__, self.model, # type: ignore self._get_objective(), @@ -53,7 +54,7 @@ def _init_acqf(self) -> None: else 1e-3, cache_root=True if isinstance(self.model, GPyTorchModel) else False, ) - return + return [acqf] def _get_objective(self) -> GenericMCObjective: # TODO: test this diff --git a/bofire/strategies/random.py b/bofire/strategies/random.py index 3ed5e1a16..9890dc081 100644 --- a/bofire/strategies/random.py +++ b/bofire/strategies/random.py @@ -1,5 +1,3 @@ -from typing import Optional - import pandas as pd from pydantic.error_wrappers import ValidationError from pydantic.types import PositiveInt @@ -43,12 +41,5 @@ def _init_sampler(self) -> None: def has_sufficient_experiments(self) -> bool: return True - def _choose_from_pool( - self, - candidate_pool: pd.DataFrame, - candidate_count: Optional[PositiveInt] = None, - ) -> pd.DataFrame: - return candidate_pool.sample(n=candidate_count) - def _ask(self, candidate_count: PositiveInt) -> pd.DataFrame: return self.sampler.ask(candidate_count) # type: ignore diff --git a/bofire/strategies/samplers/sampler.py b/bofire/strategies/samplers/sampler.py index 8f571d95d..8dbd6b7ee 100644 --- a/bofire/strategies/samplers/sampler.py +++ b/bofire/strategies/samplers/sampler.py @@ -1,3 +1,4 @@ +import math from abc import abstractmethod from copy import deepcopy @@ -30,7 +31,7 @@ def duplicate(self, domain: Domain) -> "SamplerStrategy": pass @validate_arguments - def ask(self, n: int, return_all: bool = True) -> pd.DataFrame: + def ask(self, candidate_count: int, return_all: bool = False) -> pd.DataFrame: """Generates the samples. In the case that `NChooseK` constraints are present, per combination `n` samples are generated. @@ -42,11 +43,29 @@ def ask(self, n: int, return_all: bool = True) -> pd.DataFrame: Returns: Dataframe with samples. """ + # n = candidate_count # handle here NChooseK if len(self.domain.cnstrs.get(NChooseKConstraint)) > 0: _, unused = self.domain.get_nchoosek_combinations() + + if return_all: + sampled_combinations = unused + num_samples_per_it = candidate_count + else: + if candidate_count <= len(unused): + sampled_combinations = [ + unused[i] + for i in self.rng.choice( + len(unused), size=candidate_count, replace=False + ) + ] + num_samples_per_it = 1 + else: + sampled_combinations = unused + num_samples_per_it = math.ceil(candidate_count / len(unused)) + samples = [] - for u in unused: + for u in sampled_combinations: # create new domain without the nchoosekconstraints domain = deepcopy(self.domain) domain.constraints = domain.cnstrs.get(excludes=NChooseKConstraint) @@ -57,15 +76,18 @@ def ask(self, n: int, return_all: bool = True) -> pd.DataFrame: feat.bounds = (0, 0) # setup then sampler for this situation sampler: SamplerStrategy = self.duplicate(domain=domain) - samples.append(sampler.ask(n=n)) + samples.append(sampler.ask(num_samples_per_it)) samples = pd.concat(samples, axis=0, ignore_index=True) if return_all: - return samples - return samples.sample(n=n, replace=False, ignore_index=True) + return self.domain.validate_candidates(samples, only_inputs=True) + return self.domain.validate_candidates( + samples.sample(n=candidate_count, replace=False, ignore_index=True), + only_inputs=True, + ) - samples = self._ask(n) - if len(samples) != n: - raise ValueError(f"expected {n} samples, got {len(samples)}.") + samples = self._ask(candidate_count) + if len(samples) != candidate_count: + raise ValueError(f"expected {candidate_count} samples, got {len(samples)}.") return self.domain.validate_candidates(samples, only_inputs=True) def has_sufficient_experiments(self) -> bool: diff --git a/bofire/strategies/strategy.py b/bofire/strategies/strategy.py index 4f56a1a2e..fc39f0341 100644 --- a/bofire/strategies/strategy.py +++ b/bofire/strategies/strategy.py @@ -87,7 +87,6 @@ def ask( self, candidate_count: Optional[PositiveInt] = None, add_pending: bool = False, - candidate_pool: Optional[pd.DataFrame] = None, ) -> pd.DataFrame: """Function to generate new candidates. @@ -95,8 +94,7 @@ def ask( candidate_count (PositiveInt, optional): Number of candidates to be generated. If not provided, the number of candidates is determined automatically. Defaults to None. add_pending (bool, optional): If true the proposed candidates are added to the set of pending experiments. Defaults to False. - candidate_pool (pd.DataFrame, optional): Pool of candidates from which a final set of candidates should be chosen. If not provided, - pool independent candidates are provided. Defaults to None. + Raises: ValueError: if candidate count is smaller than 1 @@ -115,15 +113,7 @@ def ask( "Not enough experiments available to execute the strategy." ) - if candidate_pool is None: - candidates = self._ask(candidate_count=candidate_count) - else: - self.domain.validate_candidates(candidate_pool, only_inputs=True) - if candidate_count is not None: - assert candidate_count <= len( - candidate_pool - ), "Number of requested candidates is larger than the pool from which they should be chosen." - candidates = self._choose_from_pool(candidate_pool, candidate_count) + candidates = self._ask(candidate_count=candidate_count) self.domain.validate_candidates(candidates=candidates, only_inputs=True) @@ -138,23 +128,6 @@ def ask( return candidates - def _choose_from_pool( - self, - candidate_pool: pd.DataFrame, - candidate_count: Optional[PositiveInt] = None, - ) -> pd.DataFrame: - """Abstract method to implement how a strategy chooses a set of candidates from a candidate pool. - - Args: - candidate_pool (pd.DataFrame): The pool of candidates from which the candidates should be chosen. - candidate_count (Optional[PositiveInt], optional): Number of candidates to choose. Defaults to None. - - Returns: - pd.DataFrame: The chosen set of candidates. - """ - # TODO: change inheritence hierarchy to make this an optional feature of a strategy provided by inheritence - raise NotImplementedError - @abstractmethod def has_sufficient_experiments( self, @@ -222,6 +195,10 @@ def add_candidates(self, candidates: pd.DataFrame): ignore_index=True, ) + def reset_candidates(self): + """Resets the pending candidates of the strategy.""" + self._candidates = None + @property def num_candidates(self) -> int: """Returns number of (pending) candidates""" diff --git a/bofire/utils/torch_tools.py b/bofire/utils/torch_tools.py index f2edb3d69..1a9fbae46 100644 --- a/bofire/utils/torch_tools.py +++ b/bofire/utils/torch_tools.py @@ -1,4 +1,4 @@ -from typing import Callable, List, Optional, Tuple, Union +from typing import Callable, Dict, List, Optional, Tuple, Union import numpy as np import torch @@ -20,6 +20,7 @@ MinimizeSigmoidObjective, TargetObjective, ) +from bofire.strategies.strategy import Strategy tkwargs = { "dtype": torch.double, @@ -101,6 +102,16 @@ def get_nchoosek_constraints(domain: Domain) -> List[Callable[[Tensor], float]]: def narrow_gaussian(x, ell=1e-3): return torch.exp(-0.5 * (x / ell) ** 2) + def max_constraint(indices: Tensor, num_features: int, max_count: int): + return lambda x: narrow_gaussian(x=x[..., indices]).sum(dim=-1) - ( + num_features - max_count + ) + + def min_constraint(indices: Tensor, num_features: int, min_count: int): + return lambda x: -narrow_gaussian(x=x[..., indices]).sum(dim=-1) + ( + num_features - min_count + ) + constraints = [] # ignore none also valid for the start for c in domain.cnstrs.get(NChooseKConstraint): @@ -111,13 +122,15 @@ def narrow_gaussian(x, ell=1e-3): ) if c.max_count != len(c.features): constraints.append( - lambda x: narrow_gaussian(x=x[..., indices]).sum(dim=-1) - - (len(c.features) - c.max_count) # type: ignore + max_constraint( + indices=indices, num_features=len(c.features), max_count=c.max_count + ) ) if c.min_count > 0: constraints.append( - lambda x: -narrow_gaussian(x=x[..., indices]).sum(dim=-1) - + (len(c.features) - c.min_count) # type: ignore + min_constraint( + indices=indices, num_features=len(c.features), min_count=c.min_count + ) ) return constraints @@ -295,3 +308,57 @@ def objective(samples: Tensor, X: Optional[Tensor] = None) -> Tensor: return torch.stack([c(samples, None) for c in callables], dim=-1) return objective + + +def get_initial_conditions_generator( + strategy: Strategy, + transform_specs: Dict, + ask_options: Dict = {}, + sequential: bool = True, +) -> Callable[[int, int, int], Tensor]: + """Takes a strategy object and returns a callable which uses this + strategy to return a generator callable which can be used in botorch`s + `gen_batch_initial_conditions` to generate samples. + + Args: + strategy (Strategy): Strategy that should be used to generate samples. + transform_specs (Dict): Dictionary indicating how the samples should be + transformed. + ask_options (Dict, optional): Dictionary of keyword arguments that are + passed to the `ask` method of the strategy. Defaults to {}. + sequential (bool, optional): If True, samples for every q-batch are + generate indepenent from each other. If False, the `n x q` samples + are generated at once. + + Returns: + Callable[[int, int, int], Tensor]: Callable that can be passed to + `batch_initial_conditions`. + """ + + def generator(n: int, q: int, seed: int) -> Tensor: + if sequential: + initial_conditions = [] + for _ in range(n): + candidates = strategy.ask(q, **ask_options) + # transform it + transformed_candidates = strategy.domain.inputs.transform( + candidates, transform_specs + ) + # transform to tensor + initial_conditions.append( + torch.from_numpy(transformed_candidates.values).to(**tkwargs) + ) + return torch.stack(initial_conditions, dim=0) + else: + candidates = strategy.ask(n * q, **ask_options) + # transform it + transformed_candidates = strategy.domain.inputs.transform( + candidates, transform_specs + ) + return ( + torch.from_numpy(transformed_candidates.values) + .to(**tkwargs) + .reshape(n, q, transformed_candidates.shape[1]) + ) + + return generator diff --git a/requirements.txt b/requirements.txt index 61135d3a0..c0cc52699 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ scikit-learn matplotlib pytest torch>=1.12 -botorch>=0.8.2 +botorch @ git+https://github.com/pytorch/botorch.git#egg=botorch multiprocess plotly formulaic>=0.5.2 diff --git a/tests/bofire/benchmarks/test_single.py b/tests/bofire/benchmarks/test_single.py index 65ae5c78b..f8c13053b 100644 --- a/tests/bofire/benchmarks/test_single.py +++ b/tests/bofire/benchmarks/test_single.py @@ -1,7 +1,19 @@ import numpy as np import pytest -from bofire.benchmarks.single import Ackley, Branin, Branin30, Himmelblau +from bofire.benchmarks.single import Ackley, Branin, Branin30, Hartmann, Himmelblau + + +def test_hartmann(): + with pytest.raises(ValueError): + Hartmann(8) + h = Hartmann(dim=6, allowed_k=3) + assert h.dim == 6 + assert h.domain.constraints[0].max_count == 3 + with pytest.raises(ValueError): + h.get_optima() + h = Hartmann(dim=6, allowed_k=None) + assert len(h.domain.constraints) == 0 @pytest.mark.parametrize( @@ -11,6 +23,8 @@ (Ackley, False, {}), (Himmelblau, True, {}), (Ackley, True, {}), + (Hartmann, True, {}), + (Hartmann, False, {}), (Branin, True, {}), (Branin, False, {}), (Branin30, True, {}), diff --git a/tests/bofire/data_models/test_samplers.py b/tests/bofire/data_models/test_samplers.py index b3793ef6f..c22984c40 100644 --- a/tests/bofire/data_models/test_samplers.py +++ b/tests/bofire/data_models/test_samplers.py @@ -149,3 +149,16 @@ def test_PolytopeSampler_all_fixed(): sampler = strategies.PolytopeSampler(data_model=data_model) with pytest.warns(UserWarning): sampler.ask(2) + + +def test_PolytopeSampler_nchoosek(): + domain = Domain( + input_features=[if1, if2, if3, if4, if6, If7], + constraints=[c6, c2], + ) + data_model = data_models.PolytopeSampler(domain=domain) + sampler = strategies.PolytopeSampler(data_model=data_model) + samples = sampler.ask(5, return_all=True) + assert len(samples) == 15 + samples = sampler.ask(50, return_all=False) + assert len(samples) == 50 diff --git a/tests/bofire/strategies/test_ask.py b/tests/bofire/strategies/test_ask.py index 1b62886bb..3f69156ae 100644 --- a/tests/bofire/strategies/test_ask.py +++ b/tests/bofire/strategies/test_ask.py @@ -41,7 +41,7 @@ def test_ask_single_objective(cls, spec, categorical, descriptor, candidate_coun random_strategy = PolytopeSampler( data_model=PolytopeSamplerDataModel(domain=benchmark.domain) ) - experiments = benchmark.f(random_strategy.ask(n=10), return_complete=True) + experiments = benchmark.f(random_strategy.ask(10), return_complete=True) # set up of the strategy data_model = cls(**{**spec, "domain": benchmark.domain}) @@ -77,7 +77,7 @@ def test_ask_multi_objective(cls, spec, use_ref_point, candidate_count): random_strategy = PolytopeSampler( data_model=PolytopeSamplerDataModel(domain=benchmark.domain) ) - experiments = benchmark.f(random_strategy.ask(n=10), return_complete=True) + experiments = benchmark.f(random_strategy.ask(10), return_complete=True) # set up of the strategy data_model = cls( diff --git a/tests/bofire/strategies/test_base.py b/tests/bofire/strategies/test_base.py index 1dfa69dad..058c3e3c5 100644 --- a/tests/bofire/strategies/test_base.py +++ b/tests/bofire/strategies/test_base.py @@ -1,22 +1,26 @@ +import itertools import unittest import warnings from typing import Literal, Type import pandas as pd import pytest +import torch +from botorch.optim.initializers import gen_batch_initial_conditions import bofire.data_models.strategies.api as data_models import bofire.data_models.surrogates.api as surrogate_data_models import bofire.strategies.api as strategies import tests.bofire.data_models.specs.api as specs +from bofire.benchmarks.single import Hartmann from bofire.data_models.constraints.api import ( Constraint, - LinearConstraint, LinearEqualityConstraint, LinearInequalityConstraint, + NChooseKConstraint, ) from bofire.data_models.domain.api import Domain -from bofire.data_models.enum import CategoricalEncodingEnum +from bofire.data_models.enum import CategoricalEncodingEnum, CategoricalMethodEnum from bofire.data_models.features.api import ( CategoricalDescriptorInput, CategoricalInput, @@ -27,6 +31,7 @@ Output, ) from bofire.data_models.objectives.api import MaximizeObjective, MinimizeObjective +from bofire.utils.torch_tools import get_nchoosek_constraints, tkwargs from tests.bofire.data_models.test_domain_validators import generate_experiments from tests.bofire.strategies.specs import ( VALID_ALLOWED_CATEGORICAL_DESCRIPTOR_INPUT_FEATURE_SPEC, @@ -63,9 +68,9 @@ class DummyStrategyDataModel(data_models.BotorchStrategy): @classmethod def is_constraint_implemented(cls, my_type: Type[Constraint]) -> bool: return my_type in [ - LinearConstraint, LinearEqualityConstraint, LinearInequalityConstraint, + NChooseKConstraint, ] @classmethod @@ -84,7 +89,7 @@ def is_objective_implemented(cls, my_type: Type[Feature]) -> bool: class DummyStrategy(strategies.BotorchStrategy): - def _init_acqf( + def _get_acqfs( self, ) -> None: pass @@ -733,3 +738,127 @@ def test_base_predict(domain, data, acquisition_function): predictions = myStrategy.predict(data) assert len(predictions.columns.tolist()) == 3 * len(domain.get_feature_keys(Output)) assert data.index[-1] == predictions.index[-1] + + +@pytest.mark.parametrize( + "categorical_method, descriptor_method, discrete_method", + list( + itertools.product( + [CategoricalMethodEnum.FREE, CategoricalMethodEnum.EXHAUSTIVE], + [CategoricalMethodEnum.FREE, CategoricalMethodEnum.EXHAUSTIVE], + [CategoricalMethodEnum.FREE, CategoricalMethodEnum.EXHAUSTIVE], + ) + ), +) +def test_base_setup_ask_fixed_features( + categorical_method, descriptor_method, discrete_method +): + # test for fixed features list + data_model = DummyStrategyDataModel( + domain=domains[0], + acquisition_function=specs.acquisition_functions.valid().obj(), + categorical_method=categorical_method, + descriptor_method=descriptor_method, + discrete_method=discrete_method, + surrogate_specs=surrogate_data_models.BotorchSurrogates( + surrogates=[ + surrogate_data_models.SingleTaskGPSurrogate( + input_features=domains[0].input_features, + output_features=domains[0].output_features, + # input_preprocessing_specs={"if5": CategoricalEncodingEnum.ONE_HOT}, + ) + ] + ), + ) + myStrategy = DummyStrategy(data_model=data_model) + ( + bounds, + ic_generator, + ic_gen_kwargs, + nchooseks, + fixed_features, + fixed_features_list, + ) = myStrategy._setup_ask() + if any( + enc == CategoricalMethodEnum.EXHAUSTIVE + for enc in [ + categorical_method, + descriptor_method, + discrete_method, + ] + ): + assert fixed_features is None + assert fixed_features_list is not None + else: + assert fixed_features == {} + assert fixed_features_list is None + + data_model = DummyStrategyDataModel( + domain=domains[3], + acquisition_function=specs.acquisition_functions.valid().obj(), + categorical_method=categorical_method, + descriptor_method=descriptor_method, + discrete_method=discrete_method, + ) + myStrategy = DummyStrategy(data_model=data_model) + ( + bounds, + ic_generator, + ic_gen_kwargs, + nchooseks, + fixed_features, + fixed_features_list, + ) = myStrategy._setup_ask() + assert fixed_features == {1: 3.0} + assert fixed_features_list is None + + +def test_base_setup_ask(): + # test for no nchooseks + benchmark = Hartmann() + data_model = DummyStrategyDataModel( + domain=benchmark.domain, + acquisition_function=specs.acquisition_functions.valid().obj(), + ) + myStrategy = DummyStrategy(data_model=data_model) + ( + bounds, + ic_generator, + ic_gen_kwargs, + nchooseks, + fixed_features, + fixed_features_list, + ) = myStrategy._setup_ask() + assert torch.allclose( + bounds, + torch.tensor([[0 for _ in range(6)], [1 for _ in range(6)]]).to(**tkwargs), + ) + assert ic_generator is None + assert ic_gen_kwargs == {} + assert nchooseks is None + assert fixed_features == {} + assert fixed_features_list is None + # test for nchooseks + benchmark = Hartmann(dim=6, allowed_k=3) + data_model = DummyStrategyDataModel( + domain=benchmark.domain, + acquisition_function=specs.acquisition_functions.valid().obj(), + ) + myStrategy = DummyStrategy(data_model=data_model) + ( + bounds, + ic_generator, + ic_gen_kwargs, + nchooseks, + fixed_features, + fixed_features_list, + ) = myStrategy._setup_ask() + assert torch.allclose( + bounds, + torch.tensor([[0 for _ in range(6)], [1 for _ in range(6)]]).to(**tkwargs), + ) + assert ic_generator == gen_batch_initial_conditions + assert list(ic_gen_kwargs.keys()) == ["generator"] + assert len(nchooseks) == len(get_nchoosek_constraints(domain=benchmark.domain)) + assert fixed_features == {} + assert fixed_features_list is None diff --git a/tests/bofire/strategies/test_base_with_nchoosek_constraint.py b/tests/bofire/strategies/test_base_with_nchoosek_constraint.py deleted file mode 100644 index ddbef2043..000000000 --- a/tests/bofire/strategies/test_base_with_nchoosek_constraint.py +++ /dev/null @@ -1,825 +0,0 @@ -import unittest - -import numpy as np -import pandas as pd -import pytest - -import bofire.data_models.strategies.api as data_models -import bofire.strategies.api as strategies -import tests.bofire.data_models.specs.api as specs -from bofire.data_models.acquisition_functions.api import qNEI -from bofire.data_models.constraints.api import NChooseKConstraint -from bofire.data_models.domain.api import Domain -from bofire.data_models.enum import CategoricalMethodEnum -from bofire.data_models.features.api import ( - CategoricalInput, - ContinuousInput, - ContinuousOutput, -) - -# NChooseK constraints 1 -cc1a = NChooseKConstraint( - features=["0", "1", "2", "3"], min_count=2, max_count=3, none_also_valid=True -) -cc2a = NChooseKConstraint( - features=["2", "3", "4", "5"], min_count=1, max_count=2, none_also_valid=True -) - -# NChooseK constraints 2 -cc1b = NChooseKConstraint( - features=["0", "1", "2", "3"], min_count=2, max_count=3, none_also_valid=False -) -cc2b = NChooseKConstraint( - features=["2", "3", "4", "5"], min_count=1, max_count=2, none_also_valid=True -) - -# NChooseK constraint 3 -cc3 = NChooseKConstraint( - features=["0", "1", "2", "3"], min_count=2, max_count=3, none_also_valid=True -) - -# input features -continuous_input_features = [] -for i in range(6): - f = ContinuousInput(key=str(i), bounds=(0, 1)) - continuous_input_features.append(f) -categorical_feature = CategoricalInput( - key="categorical_feature", categories=["c1", "c2"] -) -# categorical_descriptor_feature = CategoricalDescriptorInput( -# key="categorical_descriptor_feature", -# categories=["cd1", "cd2"], -# descriptors=["d1", "d2"], -# values=[[1.0, 1.0], [2.0, 2.0]], -# ) - -# output feature -output_features = [ContinuousOutput(key="y")] - - -""" -TEST CASES: - -CASE 1: 6 continuous features, 2 overlapping NChooseK constraints, none_also_valid: True, True -CASE 2: 6 continuous features, 2 overlapping NChooseK constraints, none_also_valid: False, True - -CASE 3: 4 continuous features, 1 NChooseK constraint, none_also_valid: True, -descriptor_method: EXHAUSTIVE, categorical_method: EXHAUSTIVE, descriptor_encoding: DESCRIPTOR, categorical_encoding: ONE_HOT - -CASE 4: 4 continuous features, 1 NChooseK constraint, none_also_valid: True, -descriptor_method: EXHAUSTIVE, categorical_method: EXHAUSTIVE, descriptor_encoding: CATEGORICAL, categorical_encoding: ONE_HOT - -CASE 5: 4 continuous features, 1 NChooseK constraint, none_also_valid: True, -descriptor_method: EXHAUSTIVE, categorical_method: EXHAUSTIVE, descriptor_encoding: DESCRIPTOR, categorical_encoding: ORDINAL - -CASE 6: 4 continuous features, 1 NChooseK constraint, none_also_valid: True, -descriptor_method: EXHAUSTIVE, categorical_method: EXHAUSTIVE, descriptor_encoding: CATEGORICAL, categorical_encoding: ORDINAL - -CASE 7: 4 continuous features, 1 NChooseK constraint, none_also_valid: True, -descriptor_method: EXHAUSTIVE, categorical_method: FREE, descriptor_encoding: DESCRIPTOR, categorical_encoding: ONE_HOT - -CASE 8: 4 continuous features, 1 NChooseK constraint, none_also_valid: True, -descriptor_method: EXHAUSTIVE, categorical_method: FREE, descriptor_encoding: CATEGORICAL, categorical_encoding: ONE_HOT - -CASE 9: 4 continuous features, 1 NChooseK constraint, none_also_valid: True, -descriptor_method: FREE, categorical_method: EXHAUSTIVE, descriptor_encoding: CATEGORICAL, categorical_encoding: ORDINAL - -CASE 10: 4 continuous features, 1 NChooseK constraint, none_also_valid: True, -descriptor_method: FREE, categorical_method: EXHAUSTIVE, descriptor_encoding: CATEGORICAL, categorical_encoding: ONE_HOT - -CASE 11: 4 continuous features, 1 NChooseK constraint, none_also_valid: True, -descriptor_method: FREE, categorical_method: EXHAUSTIVE, descriptor_encoding: DESCRIPTOR, categorical_encoding: ORDINAL - -CASE 12: 4 continuous features, 1 NChooseK constraint, none_also_valid: True, -descriptor_method: FREE, categorical_method: EXHAUSTIVE, descriptor_encoding: DESCRIPTOR, categorical_encoding: ONE_HOT - -CASE 13: 4 continuous features, 1 NChooseK constraint, none_also_valid: True, -descriptor_method: FREE, categorical_method: FREE, descriptor_encoding: CATEGORICAL, categorical_encoding: ONE_HOT - -CASE 14: 4 continuous features, 1 NChooseK constraint, none_also_valid: True, -descriptor_method: FREE, categorical_method: FREE, descriptor_encoding: DESCRIPTOR, categorical_encoding: ONE_HOT -""" - -# CASE 1 -test_fixed_values_1 = [ - {3: 0.0, 4: 0.0, 5: 0.0}, - {2: 0.0, 4: 0.0, 5: 0.0}, - {2: 0.0, 3: 0.0, 5: 0.0}, - {2: 0.0, 3: 0.0, 4: 0.0}, - {3: 0.0, 5: 0.0}, - {3: 0.0, 4: 0.0}, - {2: 0.0, 5: 0.0}, - {2: 0.0, 4: 0.0}, - {2: 0.0, 3: 0.0}, - {2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0}, - {1: 0.0, 3: 0.0, 4: 0.0, 5: 0.0}, - {1: 0.0, 4: 0.0, 5: 0.0}, - {1: 0.0, 3: 0.0, 5: 0.0}, - {1: 0.0, 3: 0.0, 4: 0.0}, - {1: 0.0, 2: 0.0, 4: 0.0, 5: 0.0}, - {1: 0.0, 2: 0.0, 5: 0.0}, - {1: 0.0, 2: 0.0, 4: 0.0}, - {0: 0.0, 3: 0.0, 4: 0.0, 5: 0.0}, - {0: 0.0, 4: 0.0, 5: 0.0}, - {0: 0.0, 3: 0.0, 5: 0.0}, - {0: 0.0, 3: 0.0, 4: 0.0}, - {0: 0.0, 2: 0.0, 4: 0.0, 5: 0.0}, - {0: 0.0, 2: 0.0, 5: 0.0}, - {0: 0.0, 2: 0.0, 4: 0.0}, - {0: 0.0, 1: 0.0, 4: 0.0, 5: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 5: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0}, -] - -# CASE 2 -test_fixed_values_2 = [ - {3: 0.0, 4: 0.0, 5: 0.0}, - {2: 0.0, 4: 0.0, 5: 0.0}, - {2: 0.0, 3: 0.0, 5: 0.0}, - {2: 0.0, 3: 0.0, 4: 0.0}, - {3: 0.0, 5: 0.0}, - {3: 0.0, 4: 0.0}, - {2: 0.0, 5: 0.0}, - {2: 0.0, 4: 0.0}, - {2: 0.0, 3: 0.0}, - {2: 0.0, 3: 0.0, 4: 0.0, 5: 0.0}, - {1: 0.0, 3: 0.0, 4: 0.0, 5: 0.0}, - {1: 0.0, 4: 0.0, 5: 0.0}, - {1: 0.0, 3: 0.0, 5: 0.0}, - {1: 0.0, 3: 0.0, 4: 0.0}, - {1: 0.0, 2: 0.0, 4: 0.0, 5: 0.0}, - {1: 0.0, 2: 0.0, 5: 0.0}, - {1: 0.0, 2: 0.0, 4: 0.0}, - {0: 0.0, 3: 0.0, 4: 0.0, 5: 0.0}, - {0: 0.0, 4: 0.0, 5: 0.0}, - {0: 0.0, 3: 0.0, 5: 0.0}, - {0: 0.0, 3: 0.0, 4: 0.0}, - {0: 0.0, 2: 0.0, 4: 0.0, 5: 0.0}, - {0: 0.0, 2: 0.0, 5: 0.0}, - {0: 0.0, 2: 0.0, 4: 0.0}, - {0: 0.0, 1: 0.0, 4: 0.0, 5: 0.0}, -] - -# CASE 3 -test_fixed_values_3 = [ - {2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 1.0}, - {1: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 1.0}, - {1: 0.0, 2: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 2: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 1.0}, - {3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 1.0}, - {2: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 1.0}, - {1: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 1.0}, - {2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 1.0}, - {1: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 1.0}, - {1: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 1.0}, - {3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 1.0}, - {2: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 1.0}, - {1: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 1.0}, - {2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 2.0, 9: 2.0}, - {1: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 2.0, 9: 2.0}, - {1: 0.0, 2: 0.0, 6: 1.0, 7: 0.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 2: 0.0, 6: 1.0, 7: 0.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 1: 0.0, 6: 1.0, 7: 0.0, 8: 2.0, 9: 2.0}, - {3: 0.0, 6: 1.0, 7: 0.0, 8: 2.0, 9: 2.0}, - {2: 0.0, 6: 1.0, 7: 0.0, 8: 2.0, 9: 2.0}, - {1: 0.0, 6: 1.0, 7: 0.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 6: 1.0, 7: 0.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 2.0, 9: 2.0}, - {2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 2.0, 9: 2.0}, - {1: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 2.0, 9: 2.0}, - {1: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 1: 0.0, 6: 0.0, 7: 1.0, 8: 2.0, 9: 2.0}, - {3: 0.0, 6: 0.0, 7: 1.0, 8: 2.0, 9: 2.0}, - {2: 0.0, 6: 0.0, 7: 1.0, 8: 2.0, 9: 2.0}, - {1: 0.0, 6: 0.0, 7: 1.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 6: 0.0, 7: 1.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 2.0, 9: 2.0}, -] - -test_fixed_values_4 = [ - {2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {1: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {1: 0.0, 2: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 2: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 1: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {2: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {1: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {1: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {1: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 1: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {2: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {1: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {1: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {1: 0.0, 2: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 2: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {3: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {2: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {1: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {1: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {1: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {3: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {2: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {1: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, -] - -test_fixed_values_5 = [ - {2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0}, - {1: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0}, - {1: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 1.0}, - {0: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0}, - {0: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 1.0}, - {0: 0.0, 1: 0.0, 6: 0.0, 7: 1.0, 8: 1.0}, - {3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0}, - {2: 0.0, 6: 0.0, 7: 1.0, 8: 1.0}, - {1: 0.0, 6: 0.0, 7: 1.0, 8: 1.0}, - {0: 0.0, 6: 0.0, 7: 1.0, 8: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0}, - {2: 0.0, 3: 0.0, 6: 1.0, 7: 1.0, 8: 1.0}, - {1: 0.0, 3: 0.0, 6: 1.0, 7: 1.0, 8: 1.0}, - {1: 0.0, 2: 0.0, 6: 1.0, 7: 1.0, 8: 1.0}, - {0: 0.0, 3: 0.0, 6: 1.0, 7: 1.0, 8: 1.0}, - {0: 0.0, 2: 0.0, 6: 1.0, 7: 1.0, 8: 1.0}, - {0: 0.0, 1: 0.0, 6: 1.0, 7: 1.0, 8: 1.0}, - {3: 0.0, 6: 1.0, 7: 1.0, 8: 1.0}, - {2: 0.0, 6: 1.0, 7: 1.0, 8: 1.0}, - {1: 0.0, 6: 1.0, 7: 1.0, 8: 1.0}, - {0: 0.0, 6: 1.0, 7: 1.0, 8: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0, 7: 1.0, 8: 1.0}, - {2: 0.0, 3: 0.0, 6: 0.0, 7: 2.0, 8: 2.0}, - {1: 0.0, 3: 0.0, 6: 0.0, 7: 2.0, 8: 2.0}, - {1: 0.0, 2: 0.0, 6: 0.0, 7: 2.0, 8: 2.0}, - {0: 0.0, 3: 0.0, 6: 0.0, 7: 2.0, 8: 2.0}, - {0: 0.0, 2: 0.0, 6: 0.0, 7: 2.0, 8: 2.0}, - {0: 0.0, 1: 0.0, 6: 0.0, 7: 2.0, 8: 2.0}, - {3: 0.0, 6: 0.0, 7: 2.0, 8: 2.0}, - {2: 0.0, 6: 0.0, 7: 2.0, 8: 2.0}, - {1: 0.0, 6: 0.0, 7: 2.0, 8: 2.0}, - {0: 0.0, 6: 0.0, 7: 2.0, 8: 2.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0, 7: 2.0, 8: 2.0}, - {2: 0.0, 3: 0.0, 6: 1.0, 7: 2.0, 8: 2.0}, - {1: 0.0, 3: 0.0, 6: 1.0, 7: 2.0, 8: 2.0}, - {1: 0.0, 2: 0.0, 6: 1.0, 7: 2.0, 8: 2.0}, - {0: 0.0, 3: 0.0, 6: 1.0, 7: 2.0, 8: 2.0}, - {0: 0.0, 2: 0.0, 6: 1.0, 7: 2.0, 8: 2.0}, - {0: 0.0, 1: 0.0, 6: 1.0, 7: 2.0, 8: 2.0}, - {3: 0.0, 6: 1.0, 7: 2.0, 8: 2.0}, - {2: 0.0, 6: 1.0, 7: 2.0, 8: 2.0}, - {1: 0.0, 6: 1.0, 7: 2.0, 8: 2.0}, - {0: 0.0, 6: 1.0, 7: 2.0, 8: 2.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0, 7: 2.0, 8: 2.0}, -] - -test_fixed_values_6 = [ - {2: 0.0, 3: 0.0, 6: 0.0, 7: 0.0}, - {1: 0.0, 3: 0.0, 6: 0.0, 7: 0.0}, - {1: 0.0, 2: 0.0, 6: 0.0, 7: 0.0}, - {0: 0.0, 3: 0.0, 6: 0.0, 7: 0.0}, - {0: 0.0, 2: 0.0, 6: 0.0, 7: 0.0}, - {0: 0.0, 1: 0.0, 6: 0.0, 7: 0.0}, - {3: 0.0, 6: 0.0, 7: 0.0}, - {2: 0.0, 6: 0.0, 7: 0.0}, - {1: 0.0, 6: 0.0, 7: 0.0}, - {0: 0.0, 6: 0.0, 7: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0, 7: 0.0}, - {2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0}, - {1: 0.0, 3: 0.0, 6: 1.0, 7: 0.0}, - {1: 0.0, 2: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 3: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 2: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 1: 0.0, 6: 1.0, 7: 0.0}, - {3: 0.0, 6: 1.0, 7: 0.0}, - {2: 0.0, 6: 1.0, 7: 0.0}, - {1: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0}, - {2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0}, - {1: 0.0, 3: 0.0, 6: 0.0, 7: 1.0}, - {1: 0.0, 2: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 3: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 2: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 1: 0.0, 6: 0.0, 7: 1.0}, - {3: 0.0, 6: 0.0, 7: 1.0}, - {2: 0.0, 6: 0.0, 7: 1.0}, - {1: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0}, - {2: 0.0, 3: 0.0, 6: 1.0, 7: 1.0}, - {1: 0.0, 3: 0.0, 6: 1.0, 7: 1.0}, - {1: 0.0, 2: 0.0, 6: 1.0, 7: 1.0}, - {0: 0.0, 3: 0.0, 6: 1.0, 7: 1.0}, - {0: 0.0, 2: 0.0, 6: 1.0, 7: 1.0}, - {0: 0.0, 1: 0.0, 6: 1.0, 7: 1.0}, - {3: 0.0, 6: 1.0, 7: 1.0}, - {2: 0.0, 6: 1.0, 7: 1.0}, - {1: 0.0, 6: 1.0, 7: 1.0}, - {0: 0.0, 6: 1.0, 7: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0, 7: 1.0}, -] - -test_fixed_values_7 = [ - {2: 0.0, 3: 0.0, 8: 1.0, 9: 1.0}, - {1: 0.0, 3: 0.0, 8: 1.0, 9: 1.0}, - {1: 0.0, 2: 0.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 3: 0.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 2: 0.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 8: 1.0, 9: 1.0}, - {3: 0.0, 8: 1.0, 9: 1.0}, - {2: 0.0, 8: 1.0, 9: 1.0}, - {1: 0.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 8: 1.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 8: 1.0, 9: 1.0}, - {2: 0.0, 3: 0.0, 8: 2.0, 9: 2.0}, - {1: 0.0, 3: 0.0, 8: 2.0, 9: 2.0}, - {1: 0.0, 2: 0.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 3: 0.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 2: 0.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 1: 0.0, 8: 2.0, 9: 2.0}, - {3: 0.0, 8: 2.0, 9: 2.0}, - {2: 0.0, 8: 2.0, 9: 2.0}, - {1: 0.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 8: 2.0, 9: 2.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 8: 2.0, 9: 2.0}, -] - -test_fixed_values_8 = [ - {2: 0.0, 3: 0.0}, - {1: 0.0, 3: 0.0}, - {1: 0.0, 2: 0.0}, - {0: 0.0, 3: 0.0}, - {0: 0.0, 2: 0.0}, - {0: 0.0, 1: 0.0}, - {3: 0.0}, - {2: 0.0}, - {1: 0.0}, - {0: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0}, -] - -test_fixed_values_9 = [ - {2: 0.0, 3: 0.0, 6: 0.0, 7: 0.0}, - {1: 0.0, 3: 0.0, 6: 0.0, 7: 0.0}, - {1: 0.0, 2: 0.0, 6: 0.0, 7: 0.0}, - {0: 0.0, 3: 0.0, 6: 0.0, 7: 0.0}, - {0: 0.0, 2: 0.0, 6: 0.0, 7: 0.0}, - {0: 0.0, 1: 0.0, 6: 0.0, 7: 0.0}, - {3: 0.0, 6: 0.0, 7: 0.0}, - {2: 0.0, 6: 0.0, 7: 0.0}, - {1: 0.0, 6: 0.0, 7: 0.0}, - {0: 0.0, 6: 0.0, 7: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0, 7: 0.0}, - {2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0}, - {1: 0.0, 3: 0.0, 6: 1.0, 7: 0.0}, - {1: 0.0, 2: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 3: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 2: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 1: 0.0, 6: 1.0, 7: 0.0}, - {3: 0.0, 6: 1.0, 7: 0.0}, - {2: 0.0, 6: 1.0, 7: 0.0}, - {1: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0}, - {2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0}, - {1: 0.0, 3: 0.0, 6: 0.0, 7: 1.0}, - {1: 0.0, 2: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 3: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 2: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 1: 0.0, 6: 0.0, 7: 1.0}, - {3: 0.0, 6: 0.0, 7: 1.0}, - {2: 0.0, 6: 0.0, 7: 1.0}, - {1: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0}, - {2: 0.0, 3: 0.0, 6: 1.0, 7: 1.0}, - {1: 0.0, 3: 0.0, 6: 1.0, 7: 1.0}, - {1: 0.0, 2: 0.0, 6: 1.0, 7: 1.0}, - {0: 0.0, 3: 0.0, 6: 1.0, 7: 1.0}, - {0: 0.0, 2: 0.0, 6: 1.0, 7: 1.0}, - {0: 0.0, 1: 0.0, 6: 1.0, 7: 1.0}, - {3: 0.0, 6: 1.0, 7: 1.0}, - {2: 0.0, 6: 1.0, 7: 1.0}, - {1: 0.0, 6: 1.0, 7: 1.0}, - {0: 0.0, 6: 1.0, 7: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0, 7: 1.0}, -] - -test_fixed_values_10 = [ - {2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {1: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {1: 0.0, 2: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 2: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 1: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {2: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {1: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 1.0, 9: 0.0}, - {2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {1: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {1: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 1: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {2: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {1: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 1.0, 9: 0.0}, - {2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {1: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {1: 0.0, 2: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 2: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {3: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {2: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {1: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0, 8: 0.0, 9: 1.0}, - {2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {1: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {1: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 2: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {3: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {2: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {1: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0, 8: 0.0, 9: 1.0}, -] - -test_fixed_values_11 = [ - {2: 0.0, 3: 0.0, 6: 0.0}, - {1: 0.0, 3: 0.0, 6: 0.0}, - {1: 0.0, 2: 0.0, 6: 0.0}, - {0: 0.0, 3: 0.0, 6: 0.0}, - {0: 0.0, 2: 0.0, 6: 0.0}, - {0: 0.0, 1: 0.0, 6: 0.0}, - {3: 0.0, 6: 0.0}, - {2: 0.0, 6: 0.0}, - {1: 0.0, 6: 0.0}, - {0: 0.0, 6: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0}, - {2: 0.0, 3: 0.0, 6: 1.0}, - {1: 0.0, 3: 0.0, 6: 1.0}, - {1: 0.0, 2: 0.0, 6: 1.0}, - {0: 0.0, 3: 0.0, 6: 1.0}, - {0: 0.0, 2: 0.0, 6: 1.0}, - {0: 0.0, 1: 0.0, 6: 1.0}, - {3: 0.0, 6: 1.0}, - {2: 0.0, 6: 1.0}, - {1: 0.0, 6: 1.0}, - {0: 0.0, 6: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0}, -] - -test_fixed_values_12 = [ - {2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0}, - {1: 0.0, 3: 0.0, 6: 1.0, 7: 0.0}, - {1: 0.0, 2: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 3: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 2: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 1: 0.0, 6: 1.0, 7: 0.0}, - {3: 0.0, 6: 1.0, 7: 0.0}, - {2: 0.0, 6: 1.0, 7: 0.0}, - {1: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 6: 1.0, 7: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 1.0, 7: 0.0}, - {2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0}, - {1: 0.0, 3: 0.0, 6: 0.0, 7: 1.0}, - {1: 0.0, 2: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 3: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 2: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 1: 0.0, 6: 0.0, 7: 1.0}, - {3: 0.0, 6: 0.0, 7: 1.0}, - {2: 0.0, 6: 0.0, 7: 1.0}, - {1: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 6: 0.0, 7: 1.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 6: 0.0, 7: 1.0}, -] - -test_fixed_values_13 = [ - {2: 0.0, 3: 0.0}, - {1: 0.0, 3: 0.0}, - {1: 0.0, 2: 0.0}, - {0: 0.0, 3: 0.0}, - {0: 0.0, 2: 0.0}, - {0: 0.0, 1: 0.0}, - {3: 0.0}, - {2: 0.0}, - {1: 0.0}, - {0: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0}, -] - -test_fixed_values_14 = [ - {2: 0.0, 3: 0.0}, - {1: 0.0, 3: 0.0}, - {1: 0.0, 2: 0.0}, - {0: 0.0, 3: 0.0}, - {0: 0.0, 2: 0.0}, - {0: 0.0, 1: 0.0}, - {3: 0.0}, - {2: 0.0}, - {1: 0.0}, - {0: 0.0}, - {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0}, -] - -# experiments -experiments = pd.DataFrame( - np.random.uniform(size=(24, 7)), columns=["0", "1", "2", "3", "4", "5", "y"] -) -experiments["categorical_feature"] = ["c1"] * 12 + ["c2"] * 12 -experiments["categorical_descriptor_feature"] = (["cd1"] * 6 + ["cd2"] * 6) * 2 -experiments["valid_y"] = 1 - - -##### LIST OF TASTE CASES ##### - -test_cases = [] - -# CASE 1 -test_case = {} -domain = Domain( - input_features=continuous_input_features, - output_features=output_features, - constraints=[cc1a, cc2a], -) -test_case["domain"] = domain -test_case["experiments"] = experiments -test_case["descriptor_method"] = CategoricalMethodEnum.EXHAUSTIVE -test_case["categorical_method"] = CategoricalMethodEnum.EXHAUSTIVE -test_case["test_fixed_values"] = test_fixed_values_1 -test_cases.append(test_case) - -# CASE 2 -test_case = {} -domain = Domain( - input_features=continuous_input_features, - output_features=output_features, - constraints=[cc1b, cc2b], -) -test_case["domain"] = domain -test_case["experiments"] = experiments -test_case["descriptor_method"] = CategoricalMethodEnum.EXHAUSTIVE -test_case["categorical_method"] = CategoricalMethodEnum.EXHAUSTIVE -test_case["test_fixed_values"] = test_fixed_values_2 -test_cases.append(test_case) - -# # CASE 3 -# test_case = {} -# domain = Domain( -# input_features=continuous_input_features -# + [categorical_feature, categorical_descriptor_feature], -# output_features=output_features, -# constraints=[cc3], -# ) -# test_case["domain"] = domain -# test_case["experiments"] = experiments -# test_case["descriptor_method"] = DescriptorMethodEnum.EXHAUSTIVE -# test_case["categorical_method"] = CategoricalMethodEnum.EXHAUSTIVE -# test_case["descriptor_encoding"] = DescriptorEncodingEnum.DESCRIPTOR -# test_case["categorical_encoding"] = CategoricalEncodingEnum.ONE_HOT -# test_case["test_fixed_values"] = test_fixed_values_3 -# test_cases.append(test_case) - -# # CASE 4 -# test_case = {} -# domain = Domain( -# input_features=continuous_input_features -# + [categorical_feature, categorical_descriptor_feature], -# output_features=output_features, -# constraints=[cc3], -# ) -# test_case["domain"] = domain -# test_case["experiments"] = experiments -# test_case["descriptor_method"] = DescriptorMethodEnum.EXHAUSTIVE -# test_case["categorical_method"] = CategoricalMethodEnum.EXHAUSTIVE -# test_case["descriptor_encoding"] = DescriptorEncodingEnum.CATEGORICAL -# test_case["categorical_encoding"] = CategoricalEncodingEnum.ONE_HOT -# test_case["test_fixed_values"] = test_fixed_values_4 -# test_cases.append(test_case) - -# # CASE 5 -# test_case = {} -# domain = Domain( -# input_features=continuous_input_features -# + [categorical_feature, categorical_descriptor_feature], -# output_features=output_features, -# constraints=[cc3], -# ) -# test_case["domain"] = domain -# test_case["experiments"] = experiments -# test_case["descriptor_method"] = DescriptorMethodEnum.EXHAUSTIVE -# test_case["categorical_method"] = CategoricalMethodEnum.EXHAUSTIVE -# test_case["descriptor_encoding"] = DescriptorEncodingEnum.DESCRIPTOR -# test_case["categorical_encoding"] = CategoricalEncodingEnum.ORDINAL -# test_case["test_fixed_values"] = test_fixed_values_5 -# test_cases.append(test_case) - -# # CASE 6 -# test_case = {} -# domain = Domain( -# input_features=continuous_input_features -# + [categorical_feature, categorical_descriptor_feature], -# output_features=output_features, -# constraints=[cc3], -# ) -# test_case["domain"] = domain -# test_case["experiments"] = experiments -# test_case["descriptor_method"] = DescriptorMethodEnum.EXHAUSTIVE -# test_case["categorical_method"] = CategoricalMethodEnum.EXHAUSTIVE -# test_case["descriptor_encoding"] = DescriptorEncodingEnum.CATEGORICAL -# test_case["categorical_encoding"] = CategoricalEncodingEnum.ORDINAL -# test_case["test_fixed_values"] = test_fixed_values_6 -# test_cases.append(test_case) - -# # CASE 7 -# test_case = {} -# domain = Domain( -# input_features=continuous_input_features -# + [categorical_feature, categorical_descriptor_feature], -# output_features=output_features, -# constraints=[cc3], -# ) -# test_case["domain"] = domain -# test_case["experiments"] = experiments -# test_case["descriptor_method"] = DescriptorMethodEnum.EXHAUSTIVE -# test_case["categorical_method"] = CategoricalMethodEnum.FREE -# test_case["descriptor_encoding"] = DescriptorEncodingEnum.DESCRIPTOR -# test_case["categorical_encoding"] = CategoricalEncodingEnum.ONE_HOT -# test_case["test_fixed_values"] = test_fixed_values_7 -# test_cases.append(test_case) - -# # CASE 8 -# test_case = {} -# domain = Domain( -# input_features=continuous_input_features -# + [categorical_feature, categorical_descriptor_feature], -# output_features=output_features, -# constraints=[cc3], -# ) -# test_case["domain"] = domain -# test_case["experiments"] = experiments -# test_case["descriptor_method"] = DescriptorMethodEnum.EXHAUSTIVE -# test_case["categorical_method"] = CategoricalMethodEnum.FREE -# test_case["descriptor_encoding"] = DescriptorEncodingEnum.CATEGORICAL -# test_case["categorical_encoding"] = CategoricalEncodingEnum.ONE_HOT -# test_case["test_fixed_values"] = test_fixed_values_8 -# test_cases.append(test_case) - -# # CASE 9 -# test_case = {} -# domain = Domain( -# input_features=continuous_input_features -# + [categorical_feature, categorical_descriptor_feature], -# output_features=output_features, -# constraints=[cc3], -# ) -# test_case["domain"] = domain -# test_case["experiments"] = experiments -# test_case["descriptor_method"] = DescriptorMethodEnum.FREE -# test_case["categorical_method"] = CategoricalMethodEnum.EXHAUSTIVE -# test_case["descriptor_encoding"] = DescriptorEncodingEnum.CATEGORICAL -# test_case["categorical_encoding"] = CategoricalEncodingEnum.ORDINAL -# test_case["test_fixed_values"] = test_fixed_values_9 -# test_cases.append(test_case) - -# # CASE 10 -# test_case = {} -# domain = Domain( -# input_features=continuous_input_features -# + [categorical_feature, categorical_descriptor_feature], -# output_features=output_features, -# constraints=[cc3], -# ) -# test_case["domain"] = domain -# test_case["experiments"] = experiments -# test_case["descriptor_method"] = DescriptorMethodEnum.FREE -# test_case["categorical_method"] = CategoricalMethodEnum.EXHAUSTIVE -# test_case["descriptor_encoding"] = DescriptorEncodingEnum.CATEGORICAL -# test_case["categorical_encoding"] = CategoricalEncodingEnum.ONE_HOT -# test_case["test_fixed_values"] = test_fixed_values_10 -# test_cases.append(test_case) - -# # CASE 11 -# test_case = {} -# domain = Domain( -# input_features=continuous_input_features -# + [categorical_feature, categorical_descriptor_feature], -# output_features=output_features, -# constraints=[cc3], -# ) -# test_case["domain"] = domain -# test_case["experiments"] = experiments -# test_case["descriptor_method"] = DescriptorMethodEnum.FREE -# test_case["categorical_method"] = CategoricalMethodEnum.EXHAUSTIVE -# test_case["descriptor_encoding"] = DescriptorEncodingEnum.DESCRIPTOR -# test_case["categorical_encoding"] = CategoricalEncodingEnum.ORDINAL -# test_case["test_fixed_values"] = test_fixed_values_11 -# test_cases.append(test_case) - -# # CASE 12 -# test_case = {} -# domain = Domain( -# input_features=continuous_input_features -# + [categorical_feature, categorical_descriptor_feature], -# output_features=output_features, -# constraints=[cc3], -# ) -# test_case["domain"] = domain -# test_case["experiments"] = experiments -# test_case["descriptor_method"] = DescriptorMethodEnum.FREE -# test_case["categorical_method"] = CategoricalMethodEnum.EXHAUSTIVE -# test_case["descriptor_encoding"] = DescriptorEncodingEnum.DESCRIPTOR -# test_case["categorical_encoding"] = CategoricalEncodingEnum.ONE_HOT -# test_case["test_fixed_values"] = test_fixed_values_12 -# test_cases.append(test_case) - -# # CASE 13 -# test_case = {} -# domain = Domain( -# input_features=continuous_input_features -# + [categorical_feature, categorical_descriptor_feature], -# output_features=output_features, -# constraints=[cc3], -# ) -# test_case["domain"] = domain -# test_case["experiments"] = experiments -# test_case["descriptor_method"] = DescriptorMethodEnum.FREE -# test_case["categorical_method"] = CategoricalMethodEnum.FREE -# test_case["descriptor_encoding"] = DescriptorEncodingEnum.CATEGORICAL -# test_case["categorical_encoding"] = CategoricalEncodingEnum.ONE_HOT -# test_case["test_fixed_values"] = test_fixed_values_13 -# test_cases.append(test_case) - -# # CASE 14 -# test_case = {} -# domain = Domain( -# input_features=continuous_input_features -# + [categorical_feature, categorical_descriptor_feature], -# output_features=output_features, -# constraints=[cc3], -# ) -# test_case["domain"] = domain -# test_case["experiments"] = experiments -# test_case["descriptor_method"] = DescriptorMethodEnum.FREE -# test_case["categorical_method"] = CategoricalMethodEnum.FREE -# test_case["descriptor_encoding"] = DescriptorEncodingEnum.DESCRIPTOR -# test_case["categorical_encoding"] = CategoricalEncodingEnum.ONE_HOT -# test_case["test_fixed_values"] = test_fixed_values_14 -# test_cases.append(test_case) - - -@pytest.mark.parametrize("test_case", test_cases) -def test_concurrency_fixed_values(test_case): - # experiments = generate_experiments(domain=test_case["domain"]) - - data_model = data_models.SoboStrategy( - domain=test_case["domain"], - acquisition_function=specs.acquisition_functions.valid(qNEI).obj(), - descriptor_method=test_case["descriptor_method"], - categorical_method=test_case["categorical_method"], - ) - sobo = strategies.map(data_model) - - fixed_values = sobo.get_fixed_values_list() - c = unittest.TestCase() - c.assertCountEqual(test_case["test_fixed_values"], fixed_values) diff --git a/tests/bofire/strategies/test_qehvi.py b/tests/bofire/strategies/test_qehvi.py index d9d58d679..f1ffaa7fc 100644 --- a/tests/bofire/strategies/test_qehvi.py +++ b/tests/bofire/strategies/test_qehvi.py @@ -147,9 +147,6 @@ def test_qehvi(strategy, use_ref_point, num_test_candidates): data_model=PolytopeSamplerDataModel(domain=benchmark.domain) ) experiments = benchmark.f(random_strategy._ask(n=10), return_complete=True) - experiments_test = benchmark.f( - random_strategy._ask(n=num_test_candidates), return_complete=True - ) # init strategy data_model = strategy( domain=benchmark.domain, @@ -157,16 +154,19 @@ def test_qehvi(strategy, use_ref_point, num_test_candidates): ) my_strategy = strategies.map(data_model) my_strategy.tell(experiments) - assert isinstance(my_strategy.acqf.objective, GenericMCMultiOutputObjective) + + acqf = my_strategy._get_acqfs(2)[0] + + assert isinstance(acqf.objective, GenericMCMultiOutputObjective) assert isinstance( - my_strategy.acqf, + acqf, qExpectedHypervolumeImprovement if strategy == data_models.QehviStrategy else qNoisyExpectedHypervolumeImprovement, ) # test acqf calc - acqf_vals = my_strategy._choose_from_pool(experiments_test, num_test_candidates) - assert acqf_vals.shape[0] == num_test_candidates + # acqf_vals = my_strategy._choose_from_pool(experiments_test, num_test_candidates) + # assert acqf_vals.shape[0] == num_test_candidates def test_qnehvi_constraints(): @@ -178,15 +178,15 @@ def test_qnehvi_constraints(): data_model = data_models.QnehviStrategy( domain=benchmark.domain, ref_point={"f_0": 1.1, "f_1": 1.1} ) - print("data model:", data_model) my_strategy = strategies.map(data_model) my_strategy.tell(experiments) - assert isinstance(my_strategy.acqf.objective, GenericMCMultiOutputObjective) - assert isinstance(my_strategy.acqf, qNoisyExpectedHypervolumeImprovement) - assert my_strategy.acqf.eta == torch.tensor(1e-3) - assert len(my_strategy.acqf.constraints) == 1 + acqf = my_strategy._get_acqfs(2)[0] + assert isinstance(acqf.objective, GenericMCMultiOutputObjective) + assert isinstance(acqf, qNoisyExpectedHypervolumeImprovement) + assert acqf.eta == torch.tensor(1e-3) + assert len(acqf.constraints) == 1 assert torch.allclose( - my_strategy.acqf.ref_point, + acqf.ref_point, torch.tensor([-1.1, -1.1], dtype=torch.double), ) @@ -234,3 +234,18 @@ def test_get_acqf_input(strategy, ref_point, num_experiments, num_candidates): num_candidates, len(set(chain(*names.values()))), ) + + +def test_no_objective(): + domain = DTLZ2(dim=6).domain + experiments = DTLZ2(dim=6).f(domain.inputs.sample(10), return_complete=True) + domain.outputs.features.append(ContinuousOutput(key="ignore", objective=None)) + experiments["ignore"] = experiments["f_0"] + 6 + experiments["valid_ignore"] = 1 + data_model = data_models.QehviStrategy( + domain=domain, ref_point={"f_0": 1.1, "f_1": 1.1} + ) + recommender = strategies.map(data_model=data_model) + recommender.tell(experiments=experiments) + candidates = recommender.ask(candidate_count=1) + recommender.to_candidates(candidates) diff --git a/tests/bofire/strategies/test_qparego.py b/tests/bofire/strategies/test_qparego.py index 4ad5500a6..16f6a4932 100644 --- a/tests/bofire/strategies/test_qparego.py +++ b/tests/bofire/strategies/test_qparego.py @@ -3,6 +3,7 @@ import pytest import torch +from botorch.acquisition.objective import ConstrainedMCObjective, GenericMCObjective from pydantic import ValidationError import bofire.data_models.strategies.api as data_models @@ -118,6 +119,11 @@ def test_qparego(num_test_candidates): data_model = data_models.QparegoStrategy(domain=benchmark.domain) my_strategy = QparegoStrategy(data_model=data_model) my_strategy.tell(experiments) + # test get objective + objective = my_strategy._get_objective() + assert isinstance(objective, GenericMCObjective) + acqfs = my_strategy._get_acqfs(2) + assert len(acqfs) == 2 # ask candidates = my_strategy.ask(num_test_candidates) assert len(candidates) == num_test_candidates @@ -138,6 +144,9 @@ def test_qparego_constraints(num_test_candidates): data_model = data_models.QparegoStrategy(domain=benchmark.domain) my_strategy = QparegoStrategy(data_model=data_model) my_strategy.tell(experiments) + # test get objective + objective = my_strategy._get_objective() + assert isinstance(objective, ConstrainedMCObjective) # ask candidates = my_strategy.ask(num_test_candidates) assert len(candidates) == num_test_candidates @@ -200,25 +209,3 @@ def test_get_acqf_input(specs, benchmark, num_experiments, num_candidates): num_candidates, len(set(chain(*names.values()))), ) - - -# def test_qparego_constraints(): -# benchmark = C2DTLZ2(dim=4) -# random_strategy = PolytopeSampler(domain=benchmark.domain) -# experiments = benchmark.f(random_strategy._ask(n=10), return_complete=True) -# my_strategy = QparegoStrategy(domain=benchmark.domain) -# my_strategy.tell(experiments) -# assert isinstance(my_strategy.objective, WeightedMCMultiOutputObjective) -# assert isinstance(my_strategy.acqf, qNoisyExpectedHypervolumeImprovement) -# assert my_strategy.acqf.eta == torch.tensor(1e-3) -# assert len(my_strategy.acqf.constraints) == 1 -# assert torch.allclose( -# my_strategy.acqf.objective.outcomes, torch.tensor([0, 1], dtype=torch.int64) -# ) -# assert torch.allclose( -# my_strategy.acqf.objective.weights, torch.tensor([-1, -1], dtype=torch.double) -# ) -# assert torch.allclose( -# my_strategy.acqf.ref_point, -# torch.tensor([-1.1, -1.1], dtype=torch.double), -# ) diff --git a/tests/bofire/strategies/test_sobo.py b/tests/bofire/strategies/test_sobo.py index 27a110c43..1f5aaf35f 100644 --- a/tests/bofire/strategies/test_sobo.py +++ b/tests/bofire/strategies/test_sobo.py @@ -58,7 +58,7 @@ def test_SOBO_not_fitted(domain, acqf): msg = "Model not trained." with pytest.raises(AssertionError, match=msg): - strategy._init_acqf() + strategy._get_acqfs(2) @pytest.mark.parametrize( @@ -75,7 +75,7 @@ def test_SOBO_not_fitted(domain, acqf): for num_test_candidates in range(1, 3) ], ) -def test_SOBO_init_acqf(acqf, expected, num_test_candidates): +def test_SOBO_get_acqf(acqf, expected, num_test_candidates): # generate data benchmark = Himmelblau() @@ -83,10 +83,7 @@ def test_SOBO_init_acqf(acqf, expected, num_test_candidates): data_model=PolytopeSamplerDataModel(domain=benchmark.domain) ) - experiments = benchmark.f(random_strategy.ask(n=20), return_complete=True) - experiments_test = benchmark.f( - random_strategy._ask(n=num_test_candidates), return_complete=True - ) + experiments = benchmark.f(random_strategy.ask(20), return_complete=True) data_model = data_models.SoboStrategy( domain=benchmark.domain, acquisition_function=acqf @@ -94,10 +91,26 @@ def test_SOBO_init_acqf(acqf, expected, num_test_candidates): strategy = SoboStrategy(data_model=data_model) strategy.tell(experiments) - assert isinstance(strategy.acqf, expected) - # test acqf calc - acqf_vals = strategy._choose_from_pool(experiments_test, num_test_candidates) - assert acqf_vals.shape[0] == num_test_candidates + + acqfs = strategy._get_acqfs(2) + assert len(acqfs) == 1 + + assert isinstance(acqfs[0], expected) + + +def test_SOBO_calc_acquisition(): + benchmark = Himmelblau() + experiments = benchmark.f(benchmark.domain.inputs.sample(10), return_complete=True) + samples = benchmark.domain.inputs.sample(2) + data_model = data_models.SoboStrategy( + domain=benchmark.domain, acquisition_function=qEI() + ) + strategy = SoboStrategy(data_model=data_model) + strategy.tell(experiments=experiments) + vals = strategy.calc_acquisition(samples) + assert len(vals) == 2 + vals = strategy.calc_acquisition(samples, combined=True) + assert len(vals) == 1 def test_SOBO_init_qUCB(): @@ -116,7 +129,9 @@ def test_SOBO_init_qUCB(): ) strategy = SoboStrategy(data_model=data_model) strategy.tell(experiments) - assert strategy.acqf.beta_prime == math.sqrt(beta * math.pi / 2) + + acqf = strategy._get_acqfs(2)[0] + assert acqf.beta_prime == math.sqrt(beta * math.pi / 2) @pytest.mark.parametrize( diff --git a/tests/bofire/strategies/test_strategy.py b/tests/bofire/strategies/test_strategy.py index 5f693af25..8bf419689 100644 --- a/tests/bofire/strategies/test_strategy.py +++ b/tests/bofire/strategies/test_strategy.py @@ -293,6 +293,8 @@ def test_strategy_set_candidates(): assert_frame_equal(strategy.candidates, candidates[domain.inputs.get_keys()]) assert_frame_equal(strategy._candidates, candidates[domain.inputs.get_keys()]) assert strategy.num_candidates == 2 + strategy.reset_candidates() + assert strategy.num_candidates == 0 def test_strategy_add_candidates(): @@ -416,41 +418,6 @@ def test_ask(self: Strategy, candidate_count: int): strategy.ask(candidate_count=1) -@pytest.mark.parametrize( - "domain, experiments, candidate_pool, candidate_count", - [ - [domain, e3, generate_candidates(domain, 3), 2], - [domain, e3, generate_candidates(domain, 5), 3], - ], -) -def test_strategy_ask_valid_candidate_pool( - domain, experiments, candidate_pool, candidate_count -): - strategy = dummy.DummyStrategy( - data_model=dummy.DummyStrategyDataModel(domain=domain) - ) - strategy.tell(experiments) - strategy.ask(candidate_count=candidate_count, candidate_pool=candidate_pool) - - -@pytest.mark.parametrize( - "domain, experiments, candidate_pool, candidate_count", - [ - [domain, e3, generate_candidates(domain, 3), -1], - [domain, e3, generate_candidates(domain, 3), 4], - ], -) -def test_ask_invalid_candidate_count_request_pool( - domain, experiments, candidate_pool, candidate_count -): - strategy = dummy.DummyStrategy( - data_model=dummy.DummyStrategyDataModel(domain=domain) - ) - strategy.tell(experiments) - with pytest.raises((AssertionError, ValueError)): - strategy.ask(candidate_count=candidate_count, candidate_pool=candidate_pool) - - def test_ask_invalid_candidate_count_request(): strategy = dummy.DummyStrategy( data_model=dummy.DummyStrategyDataModel(domain=domain) diff --git a/tests/bofire/utils/test_torch_tools.py b/tests/bofire/utils/test_torch_tools.py index 53d10e6a4..3dc642d2e 100644 --- a/tests/bofire/utils/test_torch_tools.py +++ b/tests/bofire/utils/test_torch_tools.py @@ -5,13 +5,16 @@ import torch from botorch.acquisition.objective import ConstrainedMCObjective, GenericMCObjective +import bofire.strategies.api as strategies from bofire.data_models.constraints.api import ( LinearEqualityConstraint, LinearInequalityConstraint, NChooseKConstraint, ) from bofire.data_models.domain.api import Constraints, Domain, Inputs, Outputs +from bofire.data_models.enum import CategoricalEncodingEnum from bofire.data_models.features.api import ( + CategoricalDescriptorInput, CategoricalInput, ContinuousInput, ContinuousOutput, @@ -24,8 +27,10 @@ MinimizeSigmoidObjective, TargetObjective, ) +from bofire.data_models.strategies.api import PolytopeSampler from bofire.utils.torch_tools import ( get_additive_botorch_objective, + get_initial_conditions_generator, get_linear_constraints, get_multiobjective_objective, get_multiplicative_botorch_objective, @@ -395,6 +400,103 @@ def test_get_nchoosek_constraints(): assert len(constraints) == 1 samples = domain.inputs.sample(5) assert torch.all(constraints[0](torch.from_numpy(samples.values).to(**tkwargs)) < 0) + # test with two max nchoosek constraints + domain = Domain( + input_features=[ + ContinuousInput(key="x1", bounds=[0, 1]), + ContinuousInput(key="x2", bounds=[0, 1]), + ContinuousInput(key="x3", bounds=[0, 1]), + ], + output_features=[ContinuousOutput(key="y")], + constraints=[ + NChooseKConstraint( + features=["x1", "x2", "x3"], + min_count=0, + max_count=1, + none_also_valid=False, + ), + NChooseKConstraint( + features=["x1", "x2", "x3"], + min_count=0, + max_count=2, + none_also_valid=False, + ), + ], + ) + samples = torch.tensor([[1, 0, 0], [1, 1, 0], [1, 1, 1]]).to(**tkwargs) + constraints = get_nchoosek_constraints(domain=domain) + assert torch.allclose( + constraints[0](samples), torch.tensor([0.0, -1.0, -2.0]).to(**tkwargs) + ) + assert torch.allclose( + constraints[1](samples), torch.tensor([1.0, 0.0, -1.0]).to(**tkwargs) + ) + # test with two min nchoosek constraints + domain = Domain( + input_features=[ + ContinuousInput(key="x1", bounds=[0, 1]), + ContinuousInput(key="x2", bounds=[0, 1]), + ContinuousInput(key="x3", bounds=[0, 1]), + ], + output_features=[ContinuousOutput(key="y")], + constraints=[ + NChooseKConstraint( + features=["x1", "x2", "x3"], + min_count=1, + max_count=3, + none_also_valid=False, + ), + NChooseKConstraint( + features=["x1", "x2", "x3"], + min_count=2, + max_count=3, + none_also_valid=False, + ), + ], + ) + samples = torch.tensor([[1, 0, 0], [1, 1, 0], [1, 1, 1]]).to(**tkwargs) + constraints = get_nchoosek_constraints(domain=domain) + assert torch.allclose( + constraints[0](samples), torch.tensor([0.0, 1.0, 2.0]).to(**tkwargs) + ) + assert torch.allclose( + constraints[1](samples), torch.tensor([-1.0, 0.0, 1.0]).to(**tkwargs) + ) + # test with min/max and max constraint + # test with two min nchoosek constraints + domain = Domain( + input_features=[ + ContinuousInput(key="x1", bounds=[0, 1]), + ContinuousInput(key="x2", bounds=[0, 1]), + ContinuousInput(key="x3", bounds=[0, 1]), + ], + output_features=[ContinuousOutput(key="y")], + constraints=[ + NChooseKConstraint( + features=["x1", "x2", "x3"], + min_count=1, + max_count=2, + none_also_valid=False, + ), + NChooseKConstraint( + features=["x1", "x2", "x3"], + min_count=0, + max_count=2, + none_also_valid=False, + ), + ], + ) + samples = torch.tensor([[1, 0, 0], [1, 1, 0], [1, 1, 1]]).to(**tkwargs) + constraints = get_nchoosek_constraints(domain=domain) + assert torch.allclose( + constraints[0](samples), torch.tensor([1.0, 0.0, -1.0]).to(**tkwargs) + ) + assert torch.allclose( + constraints[1](samples), torch.tensor([0.0, 1.0, 2.0]).to(**tkwargs) + ) + assert torch.allclose( + constraints[2](samples), torch.tensor([1.0, 0.0, -1.0]).to(**tkwargs) + ) def test_get_multiobjective_objective(): @@ -440,3 +542,38 @@ def test_get_multiobjective_objective(): assert np.allclose(objective_forward[..., 0].detach().numpy(), reward1) assert np.allclose(objective_forward[..., 1].detach().numpy(), reward3) assert np.allclose(objective_forward[..., 2].detach().numpy(), reward4) + + +@pytest.mark.parametrize("sequential", [True, False]) +def test_get_initial_conditions_generator(sequential: bool): + input_features = Inputs( + features=[ + ContinuousInput(key="a", bounds=(0, 1)), + CategoricalDescriptorInput( + key="b", + categories=["alpha", "beta", "gamma"], + descriptors=["omega"], + values=[[0], [1], [3]], + ), + ] + ) + domain = Domain(input_features=input_features) + strategy = strategies.map(PolytopeSampler(domain=domain)) + # test with one hot encoding + generator = get_initial_conditions_generator( + strategy=strategy, + transform_specs={"b": CategoricalEncodingEnum.ONE_HOT}, + ask_options={}, + sequential=sequential, + ) + initial_conditions = generator(n=3, q=2, seed=42) + assert initial_conditions.shape == torch.Size((3, 2, 4)) + # test with descriptor encoding + generator = get_initial_conditions_generator( + strategy=strategy, + transform_specs={"b": CategoricalEncodingEnum.DESCRIPTOR}, + ask_options={}, + sequential=sequential, + ) + initial_conditions = generator(n=3, q=2, seed=42) + assert initial_conditions.shape == torch.Size((3, 2, 2)) diff --git a/tutorials/benchmarks/005-Hartmann_with_nchoosek.ipynb b/tutorials/benchmarks/005-Hartmann_with_nchoosek.ipynb new file mode 100644 index 000000000..a25beb995 --- /dev/null +++ b/tutorials/benchmarks/005-Hartmann_with_nchoosek.ipynb @@ -0,0 +1,182 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Himmelblau Benchmark\n", + "## Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/homebrew/Caskroom/miniforge/base/envs/bofire/lib/python3.10/site-packages/tqdm/auto.py:22: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "from bofire.benchmarks.single import Hartmann\n", + "from bofire.benchmarks.benchmark import run\n", + "from bofire.data_models.strategies.api import SoboStrategy, PolytopeSampler\n", + "from bofire.data_models.acquisition_functions.api import qEI\n", + "import bofire.strategies.api as strategies\n", + "from bofire.data_models.api import Domain\n", + "from functools import partial\n", + "import pandas as pd" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Random Optimization" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "run 00 with current best -0.965: 100%|██████████| 50/50 [00:00<00:00, 101.10it/s]\n" + ] + } + ], + "source": [ + "def sample(domain):\n", + " datamodel = PolytopeSampler(domain=domain)\n", + " sampler = strategies.map(data_model=datamodel)\n", + " sampled = sampler.ask(10)\n", + " return sampled\n", + "\n", + "def best(domain: Domain, experiments: pd.DataFrame) -> float:\n", + " return experiments.y.min()\n", + "\n", + "random_results = run(\n", + " Hartmann(dim=6, allowed_k=4),\n", + " strategy_factory=PolytopeSampler,\n", + " n_iterations=50,\n", + " metric=best,\n", + " initial_sampler=sample,\n", + " n_runs=1,\n", + " n_procs=1,\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## SOBO (GPEI) Optimization" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "run 00 with current best -2.030: 24%|██▍ | 12/50 [01:15<04:03, 6.40s/it]/Users/j30607/sandbox/botorch/botorch/optim/optimize.py:366: RuntimeWarning: Optimization failed in `gen_candidates_scipy` with the following warning(s):\n", + "[OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 8 and message Positive directional derivative for linesearch.'), UserWarning('SLSQP failed to converge to a solution the satisfies the non-linear constraints. Returning the feasible starting point.')]\n", + "Trying again with a new set of initial conditions.\n", + " warnings.warn(first_warn_msg, RuntimeWarning)\n", + "run 00 with current best -2.233: 26%|██▌ | 13/50 [01:29<05:13, 8.48s/it]/Users/j30607/sandbox/botorch/botorch/optim/optimize.py:366: RuntimeWarning: Optimization failed in `gen_candidates_scipy` with the following warning(s):\n", + "[OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 5 and message Singular matrix E in LSQ subproblem.')]\n", + "Trying again with a new set of initial conditions.\n", + " warnings.warn(first_warn_msg, RuntimeWarning)\n", + "run 00 with current best -2.242: 32%|███▏ | 16/50 [01:54<04:32, 8.03s/it]/Users/j30607/sandbox/botorch/botorch/optim/optimize.py:366: RuntimeWarning: Optimization failed in `gen_candidates_scipy` with the following warning(s):\n", + "[OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 5 and message Singular matrix E in LSQ subproblem.')]\n", + "Trying again with a new set of initial conditions.\n", + " warnings.warn(first_warn_msg, RuntimeWarning)\n", + "run 00 with current best -2.438: 48%|████▊ | 24/50 [02:53<02:58, 6.88s/it]/Users/j30607/sandbox/botorch/botorch/optim/optimize.py:366: RuntimeWarning: Optimization failed in `gen_candidates_scipy` with the following warning(s):\n", + "[OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 8 and message Positive directional derivative for linesearch.'), UserWarning('SLSQP failed to converge to a solution the satisfies the non-linear constraints. Returning the feasible starting point.')]\n", + "Trying again with a new set of initial conditions.\n", + " warnings.warn(first_warn_msg, RuntimeWarning)\n", + "/Users/j30607/sandbox/botorch/botorch/optim/optimize.py:390: RuntimeWarning: Optimization failed on the second try, after generating a new set of initial conditions.\n", + " warnings.warn(\n", + "run 00 with current best -2.488: 50%|█████ | 25/50 [03:05<03:29, 8.37s/it]/Users/j30607/sandbox/botorch/botorch/optim/optimize.py:366: RuntimeWarning: Optimization failed in `gen_candidates_scipy` with the following warning(s):\n", + "[OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 8 and message Positive directional derivative for linesearch.'), UserWarning('SLSQP failed to converge to a solution the satisfies the non-linear constraints. Returning the feasible starting point.'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 8 and message Positive directional derivative for linesearch.'), UserWarning('SLSQP failed to converge to a solution the satisfies the non-linear constraints. Returning the feasible starting point.')]\n", + "Trying again with a new set of initial conditions.\n", + " warnings.warn(first_warn_msg, RuntimeWarning)\n", + "/Users/j30607/sandbox/botorch/botorch/optim/optimize.py:390: RuntimeWarning: Optimization failed on the second try, after generating a new set of initial conditions.\n", + " warnings.warn(\n", + "run 00 with current best -2.629: 72%|███████▏ | 36/50 [06:06<04:57, 21.27s/it]/Users/j30607/sandbox/botorch/botorch/optim/initializers.py:403: BadInitialCandidatesWarning: Unable to find non-zero acquisition function values - initial conditions are being selected randomly.\n", + " warnings.warn(\n", + "run 00 with current best -2.629: 74%|███████▍ | 37/50 [07:05<07:02, 32.52s/it]/Users/j30607/sandbox/botorch/botorch/optim/initializers.py:403: BadInitialCandidatesWarning: Unable to find non-zero acquisition function values - initial conditions are being selected randomly.\n", + " warnings.warn(\n", + "run 00 with current best -2.629: 78%|███████▊ | 39/50 [08:10<05:33, 30.32s/it]/Users/j30607/sandbox/botorch/botorch/optim/initializers.py:403: BadInitialCandidatesWarning: Unable to find non-zero acquisition function values - initial conditions are being selected randomly.\n", + " warnings.warn(\n", + "run 00 with current best -2.629: 82%|████████▏ | 41/50 [10:08<06:44, 44.89s/it]/Users/j30607/sandbox/botorch/botorch/optim/initializers.py:403: BadInitialCandidatesWarning: Unable to find non-zero acquisition function values - initial conditions are being selected randomly.\n", + " warnings.warn(\n", + "run 00 with current best -2.629: 84%|████████▍ | 42/50 [11:06<06:31, 48.89s/it]/Users/j30607/sandbox/botorch/botorch/optim/initializers.py:403: BadInitialCandidatesWarning: Unable to find non-zero acquisition function values - initial conditions are being selected randomly.\n", + " warnings.warn(\n", + "run 00 with current best -2.629: 86%|████████▌ | 43/50 [12:04<06:00, 51.56s/it]/Users/j30607/sandbox/botorch/botorch/optim/initializers.py:403: BadInitialCandidatesWarning: Unable to find non-zero acquisition function values - initial conditions are being selected randomly.\n", + " warnings.warn(\n", + "run 00 with current best -2.629: 88%|████████▊ | 44/50 [13:03<05:21, 53.61s/it]/Users/j30607/sandbox/botorch/botorch/optim/initializers.py:403: BadInitialCandidatesWarning: Unable to find non-zero acquisition function values - initial conditions are being selected randomly.\n", + " warnings.warn(\n", + "run 00 with current best -2.629: 90%|█████████ | 45/50 [14:00<04:33, 54.68s/it]/Users/j30607/sandbox/botorch/botorch/optim/initializers.py:403: BadInitialCandidatesWarning: Unable to find non-zero acquisition function values - initial conditions are being selected randomly.\n", + " warnings.warn(\n", + "run 00 with current best -2.629: 92%|█████████▏| 46/50 [14:57<03:42, 55.51s/it]/Users/j30607/sandbox/botorch/botorch/optim/initializers.py:403: BadInitialCandidatesWarning: Unable to find non-zero acquisition function values - initial conditions are being selected randomly.\n", + " warnings.warn(\n", + "run 00 with current best -2.629: 96%|█████████▌| 48/50 [16:03<01:23, 41.55s/it]/Users/j30607/sandbox/botorch/botorch/optim/initializers.py:403: BadInitialCandidatesWarning: Unable to find non-zero acquisition function values - initial conditions are being selected randomly.\n", + " warnings.warn(\n", + "run 00 with current best -2.629: 98%|█████████▊| 49/50 [17:00<00:46, 46.35s/it]/Users/j30607/sandbox/botorch/botorch/optim/initializers.py:403: BadInitialCandidatesWarning: Unable to find non-zero acquisition function values - initial conditions are being selected randomly.\n", + " warnings.warn(\n", + "run 00 with current best -2.629: 100%|██████████| 50/50 [17:57<00:00, 21.56s/it]\n" + ] + } + ], + "source": [ + "from functools import partial\n", + "\n", + "bo_results = run(\n", + " Hartmann(dim=6, allowed_k=4),\n", + " strategy_factory=partial(SoboStrategy, acquisition_function=qEI(), raw_samples=512, num_restarts=24),\n", + " n_iterations=50,\n", + " metric=best,\n", + " initial_sampler=sample,\n", + " n_runs=1,\n", + " n_procs=1,\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "bofire", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/benchmarks/006-30dimBranin.ipynb b/tutorials/benchmarks/006-30dimBranin.ipynb new file mode 100644 index 000000000..5a654ace4 --- /dev/null +++ b/tutorials/benchmarks/006-30dimBranin.ipynb @@ -0,0 +1,147 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 30dim Branin Benchmark with SAASBO\n", + "This is a port from https://github.com/pytorch/botorch/blob/main/tutorials/saasbo.ipynb\n", + "## Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/homebrew/Caskroom/miniforge/base/envs/bofire/lib/python3.10/site-packages/tqdm/auto.py:22: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "from bofire.benchmarks.single import Branin30\n", + "from bofire.benchmarks.benchmark import run\n", + "from bofire.data_models.strategies.api import SoboStrategy, RandomStrategy, PolytopeSampler\n", + "from bofire.data_models.surrogates.api import SaasSingleTaskGPSurrogate, BotorchSurrogates\n", + "from bofire.data_models.acquisition_functions.api import qEI\n", + "import bofire.strategies.api as strategies\n", + "from bofire.data_models.api import Domain\n", + "from functools import partial\n", + "import pandas as pd" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Random Optimization" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "run 00 with current best 2.097: 100%|██████████| 10/10 [00:00<00:00, 39.90it/s]\n" + ] + } + ], + "source": [ + "def sample(domain):\n", + " datamodel = PolytopeSampler(domain=domain)\n", + " sampler = strategies.map(data_model=datamodel)\n", + " sampled = sampler.ask(10)\n", + " return sampled\n", + "\n", + "def best(domain: Domain, experiments: pd.DataFrame) -> float:\n", + " return experiments.y.min()\n", + "\n", + "random_results = run(\n", + " Branin30(),\n", + " strategy_factory=RandomStrategy,\n", + " n_iterations=10,\n", + " metric=best,\n", + " initial_sampler=sample,\n", + " n_candidates_per_proposal=5,\n", + " n_runs=1,\n", + " n_procs=1,\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## SAASBO Optimization" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "run 00 with current best 0.399: 100%|██████████| 10/10 [10:10<00:00, 61.05s/it]\n" + ] + } + ], + "source": [ + "benchmark = Branin30()\n", + "\n", + "random_results = run(\n", + " Branin30(),\n", + " strategy_factory=partial(\n", + " SoboStrategy, \n", + " acquisition_function=qEI(),\n", + " surrogate_specs=BotorchSurrogates(\n", + " surrogates=[\n", + " SaasSingleTaskGPSurrogate(input_features=benchmark.domain.inputs, output_features=benchmark.domain.outputs)])\n", + " ),\n", + " n_iterations=10,\n", + " metric=best,\n", + " initial_sampler=sample,\n", + " n_candidates_per_proposal=5,\n", + " n_runs=1,\n", + " n_procs=1,\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "bofire", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +}