Source code for baybe.recommenders.pure.nonpredictive.sampling
"""Recommenders based on sampling."""
from typing import ClassVar
import numpy as np
import pandas as pd
from attrs import define, field, fields
from attrs.validators import instance_of
from typing_extensions import override
from baybe.exceptions import InfeasibilityError
from baybe.recommenders.pure.nonpredictive.base import NonPredictiveRecommender
from baybe.searchspace import SearchSpace, SearchSpaceType, SubspaceDiscrete
from baybe.settings import Settings, active_settings
from baybe.utils.conversion import to_string
from baybe.utils.sampling_algorithms import FPSInitialization, farthest_point_sampling
[docs]
class RandomRecommender(NonPredictiveRecommender):
"""Recommends experiments randomly."""
# Class variables
compatibility: ClassVar[SearchSpaceType] = SearchSpaceType.HYBRID
# See base class.
supports_discrete_subset_generating_constraints: ClassVar[bool] = True
# See base class.
@override
def _recommend_hybrid(
self,
searchspace: SearchSpace,
candidates_exp: pd.DataFrame,
batch_size: int,
) -> pd.DataFrame:
if searchspace.type is SearchSpaceType.CONTINUOUS:
return searchspace.continuous.sample_uniform(batch_size=batch_size)
# Restrict to a random subset if subset-generating constraints are present
is_hybrid = searchspace.type is SearchSpaceType.HYBRID
if searchspace.discrete.n_subsets > 0:
masks = searchspace.discrete.sample_subset_masks(
candidates_exp,
n=1,
min_candidates=None if is_hybrid else batch_size,
)
if not masks:
raise InfeasibilityError(
"No feasible subset found for the given "
"subset-generating constraints. All subsets have fewer "
f"candidates than the requested {batch_size=}."
)
candidates_exp = candidates_exp.loc[masks[0]]
disc_random = candidates_exp.sample(
n=batch_size,
replace=is_hybrid or len(candidates_exp) < batch_size,
)
if not is_hybrid:
return disc_random
cont_random = searchspace.continuous.sample_uniform(batch_size=batch_size)
cont_random.index = disc_random.index
return pd.concat([disc_random, cont_random], axis=1)
@override
def __str__(self) -> str:
fields = [to_string("Compatibility", self.compatibility, single_line=True)]
return to_string(self.__class__.__name__, *fields)
[docs]
@define
class FPSRecommender(NonPredictiveRecommender):
"""An initial recommender that selects candidates via Farthest Point Sampling.
If the optional `fpsample <https://github.com/leonardodalinky/fpsample>`_ package is
installed, a more efficient implementation is available that can be (de-)activated
via the :attr:`~baybe.settings.Settings.use_fpsample` setting. Otherwise, a custom
fallback implementation is used.
"""
# Class variables
compatibility: ClassVar[SearchSpaceType] = SearchSpaceType.DISCRETE
# See base class.
initialization: FPSInitialization = field(
default=FPSInitialization.FARTHEST, converter=FPSInitialization
)
"""See :func:`~baybe.utils.sampling_algorithms.farthest_point_sampling`.
If the optional package 'fpsample' is used, only
:attr:`~baybe.utils.sampling_algorithms.FPSInitialization.FARTHEST`
is supported.
"""
random_tie_break: bool = field(validator=instance_of(bool), kw_only=True)
"""See :func:`~baybe.utils.sampling_algorithms.farthest_point_sampling`.
If the optional package 'fpsample' is used, only ``False`` is supported.
"""
@initialization.validator
def _validate_initialization(self, _, value):
if active_settings.use_fpsample and value is not FPSInitialization.FARTHEST:
raise ValueError(
f"'{self.__class__.__name__}' is currently using the optional "
f"'fpsample' package, which does not support the "
f"'{self.initialization}' mode. "
f"Please choose a supported initialization mode or deactivate "
f"`fpsample` usage via the '{fields(Settings)._use_fpsample.alias}' "
f"option in BayBE's settings."
)
@random_tie_break.default
def _default_random_tie_break(self) -> bool:
return self.initialization is not FPSInitialization.FARTHEST
@random_tie_break.validator
def _validate_random_tie_break(self, _, value):
if active_settings.use_fpsample and value:
raise ValueError(
f"'{self.__class__.__name__}' is currently using the optional "
f"'fpsample' package, which does not support random tie-breaking. "
f"Either disable the mechanism by passing "
f"'{fields(self.__class__).random_tie_break.name}=False' or deactivate "
f"`fpsample` usage via the '{fields(Settings)._use_fpsample.alias}' "
f"option in BayBE's settings."
)
@override
def _recommend_discrete(
self,
subspace_discrete: SubspaceDiscrete,
candidates_exp: pd.DataFrame,
batch_size: int,
) -> pd.Index:
# Fit scaler on entire search space
from sklearn.preprocessing import StandardScaler
# TODO [Scaling]: scaling should be handled by search space object
scaler = StandardScaler()
scaler.fit(subspace_discrete.comp_rep)
# Scale and sample
candidates_comp = subspace_discrete.transform(candidates_exp)
candidates_scaled = np.ascontiguousarray(scaler.transform(candidates_comp))
if active_settings.use_fpsample:
from baybe._optional.fpsample import fps_sampling
ilocs = fps_sampling(
candidates_scaled,
n_samples=batch_size,
)
else:
ilocs = farthest_point_sampling(
candidates_scaled,
batch_size,
initialization=self.initialization,
random_tie_break=self.random_tie_break,
)
return candidates_comp.index[ilocs]
@override
def __str__(self) -> str:
fields = [to_string("Compatibility", self.compatibility, single_line=True)]
return to_string(self.__class__.__name__, *fields)