Source code for baybe.recommenders.pure.bayesian.botorch.discrete

"""Discrete recommendation routines for BotorchRecommender."""

from __future__ import annotations

from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING

import numpy as np
import numpy.typing as npt
import pandas as pd

from baybe.searchspace import SubspaceDiscrete
from baybe.utils.dataframe import to_tensor

if TYPE_CHECKING:
    from torch import Tensor

    from baybe.recommenders.pure.bayesian.botorch.core import BotorchRecommender


[docs] def recommend_discrete_with_subsets( recommender: BotorchRecommender, subspace_discrete: SubspaceDiscrete, candidates_exp: pd.DataFrame, batch_size: int, ) -> pd.Index: """Recommend from a discrete space with subset-generating constraints. Splits the candidate set into subsets according to subset-generating constraints, runs optimization on each feasible subset, and returns the batch with the highest joint acquisition value. Subsets with fewer candidates than ``batch_size`` are skipped. Args: recommender: The recommender instance. subspace_discrete: The discrete subspace from which to generate recommendations. candidates_exp: The experimental representation of candidates. batch_size: The size of the recommendation batch. Returns: The dataframe indices of the recommended points. """ import torch masks: Iterable[npt.NDArray[np.bool_]] if subspace_discrete.n_subsets <= recommender.max_n_subsets: masks = subspace_discrete.subset_masks( candidates_exp, min_candidates=batch_size ) else: masks = subspace_discrete.sample_subset_masks( candidates_exp, recommender.max_n_subsets, min_candidates=batch_size ) def make_callable( mask: np.ndarray, ) -> Callable[[], tuple[pd.Index, Tensor]]: def optimize() -> tuple[pd.Index, Tensor]: subset = candidates_exp.loc[mask] idxs = recommend_discrete_without_subsets( recommender, subspace_discrete, subset, batch_size ) comp = subspace_discrete.transform(candidates_exp.loc[idxs]) with torch.no_grad(): acqf_value = recommender._botorch_acqf(to_tensor(comp).unsqueeze(0)) return idxs, acqf_value return optimize callables = (make_callable(m) for m in masks) best_idxs, _ = recommender._optimize_over_subsets(callables) return best_idxs
[docs] def recommend_discrete_without_subsets( recommender: BotorchRecommender, subspace_discrete: SubspaceDiscrete, candidates_exp: pd.DataFrame, batch_size: int, ) -> pd.Index: """Generate recommendations from a discrete search space. Args: recommender: The recommender instance. subspace_discrete: The discrete subspace from which to generate recommendations. candidates_exp: The experimental representation of all discrete candidate points to be considered. batch_size: The size of the recommendation batch. Raises: IncompatibleAcquisitionFunctionError: If a non-Monte Carlo acquisition function is used with a batch size > 1. Returns: The dataframe indices of the recommended points in the provided experimental representation. """ from baybe.acquisition.acqfs import qThompsonSampling from baybe.exceptions import ( IncompatibilityError, IncompatibleAcquisitionFunctionError, ) assert recommender._objective is not None acqf = recommender._get_acquisition_function(recommender._objective) if batch_size > 1 and not acqf.supports_batching: raise IncompatibleAcquisitionFunctionError( f"The '{recommender.__class__.__name__}' only works with Monte Carlo " f"acquisition functions for batch sizes > 1." ) if batch_size > 1 and isinstance(acqf, qThompsonSampling): raise IncompatibilityError( "Thompson sampling currently only supports a batch size of 1." ) from botorch.optim import optimize_acqf_discrete # determine the next set of points to be tested candidates_comp = subspace_discrete.transform(candidates_exp) points, _ = optimize_acqf_discrete( recommender._botorch_acqf, batch_size, to_tensor(candidates_comp) ) # retrieve the index of the points from the input dataframe # IMPROVE: The merging procedure is conceptually similar to what # `SearchSpace._match_measurement_with_searchspace_indices` does, though using # a simpler matching logic. When refactoring the SearchSpace class to # handle continuous parameters, a corresponding utility could be extracted. idxs = pd.Index( pd.merge( pd.DataFrame(points, columns=candidates_comp.columns), candidates_comp.reset_index(), on=list(candidates_comp), how="left", )["index"] ) return idxs