Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable Pending Points #319

Merged
merged 27 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e0e6369
Add test
Scienfitz Jul 16, 2024
46ba000
Enable nonpredictive recommenders
Scienfitz Jul 19, 2024
769e7bb
Enable other recommenders
Scienfitz Jul 19, 2024
770029e
Ignore warning in test
Scienfitz Jul 19, 2024
530917a
Add test for incompatible acqfs
Scienfitz Jul 19, 2024
4308efa
Add userguide for async
Scienfitz Jul 21, 2024
051f08a
Make `campaign` fixture available in doc tests
Scienfitz Jul 21, 2024
73471d8
Rework signatures
Scienfitz Jul 21, 2024
c61e5a3
Update README.md
Scienfitz Jul 25, 2024
13faee5
Update CHANGELOG.md
Scienfitz Jul 19, 2024
3195053
Update Userguide
Scienfitz Jul 30, 2024
f9c16b9
Improve text
Scienfitz Aug 28, 2024
f053911
Add defaults
Scienfitz Aug 28, 2024
342037f
Rework parameter name property
Scienfitz Aug 28, 2024
48e758e
Change nomenclature
Scienfitz Aug 28, 2024
36221c8
Add flag to enforce unique batches where relevant
Scienfitz Sep 3, 2024
297ce78
Revert "Add flag to enforce unique batches where relevant"
Scienfitz Sep 3, 2024
74cf068
Make pending experiment candidate exclusion configurable
Scienfitz Sep 3, 2024
2125dcc
Update user guide
Scienfitz Sep 3, 2024
69c95b3
Merge branch 'main' into feature/pending_points2
Scienfitz Sep 3, 2024
ad0daa8
Refine User Guide
Scienfitz Sep 4, 2024
b32cffc
Adjust docstrings
Scienfitz Sep 4, 2024
bcb68d3
Fix warning
Scienfitz Sep 4, 2024
e8c9658
Expand tests
Scienfitz Sep 5, 2024
6c92e29
Mention new flag in user guide
Scienfitz Sep 5, 2024
8597da1
Add new flag to conftest
Scienfitz Sep 6, 2024
9983981
Merge branch 'main' into feature/pending_points2
Scienfitz Sep 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Scienfitz marked this conversation as resolved.
Show resolved Hide resolved
AdrianSosic marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bandit optimization example
- `qThompsonSampling` acquisition function
- `BetaPrior` class
- `recommend` now accepts the `pending_experiments` argument, informing the algorithm
about points that were already selected for evaluation
- Pure recommenders now have the `allow_recommending_pending_experiments` flag,
controlling whether pending experiments are excluded from candidates in purely
discrete search spaces

### Changed
- The transition from experimental to computational representation no longer happens
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ Besides functionality to perform a typical recommend-measure loop, BayBE's highl
- ✨ Custom parameter encodings: Improve your campaign with domain knowledge
- 🧪 Built-in chemical encodings: Improve your campaign with chemical knowledge
- 🎯 Single and multiple targets with min, max and match objectives
- ⚙️ Custom surrogate models: For specialized problems or active learning
- 🎭 Hybrid (mixed continuous and discrete) spaces
- 🚀 Transfer learning: Mix data from multiple campaigns and accelerate optimization
- 🎰 Bandit models: Efficiently find the best among many options in noisy environments (e.g. A/B Testing)
- 🌎 Distributed workflows: Run campaigns asynchronously with pending experiments
- 🎓 Active learning: Perform smart data acquisition campaigns
- ⚙️ Custom surrogate models: Enhance your predictions through mechanistic understanding
- 📈 Comprehensive backtest, simulation and imputation utilities: Benchmark and find your best settings
- 📝 Fully typed and hypothesis-tested: Robust code base
- 🔄 All objects are fully de-/serializable: Useful for storing results in databases or use in wrappers like APIs
Expand Down
15 changes: 14 additions & 1 deletion baybe/acquisition/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
import pandas as pd
from attrs import define

from baybe.exceptions import UnidentifiedSubclassError
from baybe.exceptions import (
IncompatibleAcquisitionFunctionError,
UnidentifiedSubclassError,
)
from baybe.objectives.base import Objective
from baybe.objectives.desirability import DesirabilityObjective
from baybe.objectives.single import SingleTargetObjective
Expand Down Expand Up @@ -55,6 +58,7 @@ def to_botorch(
searchspace: SearchSpace,
objective: Objective,
measurements: pd.DataFrame,
pending_experiments: pd.DataFrame | None = None,
):
"""Create the botorch-ready representation of the function.

Expand Down Expand Up @@ -89,6 +93,15 @@ def to_botorch(
additional_params["mc_points"] = to_tensor(
self.get_integration_points(searchspace) # type: ignore[attr-defined]
)
if pending_experiments is not None:
if self.is_mc:
pending_x = searchspace.transform(pending_experiments, allow_extra=True)
AdrianSosic marked this conversation as resolved.
Show resolved Hide resolved
additional_params["X_pending"] = to_tensor(pending_x)
AVHopp marked this conversation as resolved.
Show resolved Hide resolved
Scienfitz marked this conversation as resolved.
Show resolved Hide resolved
else:
raise IncompatibleAcquisitionFunctionError(
f"Pending experiments were provided but the chosen acquisition "
f"function '{self.__class__.__name__}' does not support this."
)

# Add acquisition objective / best observed value
match objective:
Expand Down
10 changes: 10 additions & 0 deletions baybe/campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,16 @@ def add_measurements(
def recommend(
self,
batch_size: int,
pending_experiments: pd.DataFrame | None = None,
batch_quantity: int = None, # type: ignore[assignment]
) -> pd.DataFrame:
"""Provide the recommendations for the next batch of experiments.

Args:
batch_size: Number of requested recommendations.
pending_experiments: Parameter configurations specifying experiments
that are currently pending.
batch_quantity: Deprecated! Use ``batch_size`` instead.

Returns:
Dataframe containing the recommendations in experimental representation.
Expand All @@ -232,6 +237,10 @@ def recommend(
f"{batch_size=}."
)

# Invalidate cached recommendation if pending experiments are provided
if (pending_experiments is not None) and (len(pending_experiments) > 0):
self._cached_recommendation = pd.DataFrame()

# If there are cached recommendations and the batch size of those is equal to
# the previously requested one, we just return those
if len(self._cached_recommendation) == batch_size:
Expand All @@ -248,6 +257,7 @@ def recommend(
self.searchspace,
self.objective,
self._measurements_exp,
pending_experiments,
)

# Cache the recommendations
Expand Down
4 changes: 4 additions & 0 deletions baybe/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class IncompatibleSearchSpaceError(IncompatibilityError):
"""


class IncompatibleAcquisitionFunctionError(IncompatibilityError):
"""An incompatible acquisition function was selected."""


class NotEnoughPointsLeftError(Exception):
"""
More recommendations are requested than there are viable parameter configurations
Expand Down
7 changes: 5 additions & 2 deletions baybe/recommenders/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ def recommend(
self,
batch_size: int,
searchspace: SearchSpace,
objective: Objective | None,
measurements: pd.DataFrame | None,
objective: Objective | None = None,
measurements: pd.DataFrame | None = None,
pending_experiments: pd.DataFrame | None = None,
) -> pd.DataFrame:
"""Recommend a batch of points from the given search space.

Expand All @@ -36,6 +37,8 @@ def recommend(
Each row corresponds to one conducted experiment, where the parameter
columns define the experimental setting and the target columns report
the measured outcomes.
pending_experiments: Parameter configurations in "experimental
representation" specifying experiments that are currently pending.

Returns:
A dataframe containing the recommendations in experimental representation
Expand Down
10 changes: 9 additions & 1 deletion baybe/recommenders/meta/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def select_recommender(
searchspace: SearchSpace,
objective: Objective | None = None,
measurements: pd.DataFrame | None = None,
pending_experiments: pd.DataFrame | None = None,
) -> PureRecommender:
"""Select a pure recommender for the given experimentation context.

Expand All @@ -39,6 +40,8 @@ def select_recommender(
See :func:`baybe.recommenders.meta.base.MetaRecommender.recommend`.
measurements:
See :func:`baybe.recommenders.meta.base.MetaRecommender.recommend`.
pending_experiments:
See :func:`baybe.recommenders.meta.base.MetaRecommender.recommend`.

Returns:
The selected recommender.
Expand All @@ -50,13 +53,15 @@ def recommend(
searchspace: SearchSpace,
objective: Objective | None = None,
measurements: pd.DataFrame | None = None,
pending_experiments: pd.DataFrame | None = None,
) -> pd.DataFrame:
"""See :func:`baybe.recommenders.base.RecommenderProtocol.recommend`."""
recommender = self.select_recommender(
batch_size=batch_size,
searchspace=searchspace,
objective=objective,
measurements=measurements,
pending_experiments=pending_experiments,
)

# Non-predictive recommenders should not be called with an objective or
Expand All @@ -72,7 +77,10 @@ def recommend(
)

return recommender.recommend(
batch_size=batch_size, searchspace=searchspace, **optional_args
batch_size=batch_size,
searchspace=searchspace,
pending_experiments=pending_experiments,
**optional_args,
)


Expand Down
3 changes: 3 additions & 0 deletions baybe/recommenders/meta/sequential.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def select_recommender( # noqa: D102
searchspace: SearchSpace | None = None,
objective: Objective | None = None,
measurements: pd.DataFrame | None = None,
pending_experiments: pd.DataFrame | None = None,
) -> PureRecommender:
# See base class.

Expand Down Expand Up @@ -135,6 +136,7 @@ def select_recommender( # noqa: D102
searchspace: SearchSpace | None = None,
objective: Objective | None = None,
measurements: pd.DataFrame | None = None,
pending_experiments: pd.DataFrame | None = None,
) -> PureRecommender:
# See base class.

Expand Down Expand Up @@ -224,6 +226,7 @@ def select_recommender( # noqa: D102
searchspace: SearchSpace | None = None,
objective: Objective | None = None,
measurements: pd.DataFrame | None = None,
pending_experiments: pd.DataFrame | None = None,
) -> PureRecommender:
# See base class.

Expand Down
9 changes: 6 additions & 3 deletions baybe/recommenders/naive.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def recommend( # noqa: D102
searchspace: SearchSpace,
objective: Objective | None = None,
measurements: pd.DataFrame | None = None,
pending_experiments: pd.DataFrame | None = None,
) -> pd.DataFrame:
# See base class.

Expand All @@ -108,6 +109,7 @@ def recommend( # noqa: D102
searchspace=searchspace,
objective=objective,
measurements=measurements,
pending_experiments=pending_experiments,
)

# We are in a hybrid setting now
Expand All @@ -121,7 +123,6 @@ def recommend( # noqa: D102

# Get discrete candidates. The metadata flags are ignored since the search space
# is hybrid
# TODO Slight BOILERPLATE CODE, see recommender.py, ll. 47+
candidates_exp, _ = searchspace.discrete.get_candidates(
allow_repeated_recommendations=True,
allow_recommending_already_measured=True,
Expand All @@ -131,7 +132,7 @@ def recommend( # noqa: D102
if isinstance(self.disc_recommender, BayesianRecommender):
# Get access to the recommenders acquisition function
self.disc_recommender._setup_botorch_acqf(
searchspace, objective, measurements
searchspace, objective, measurements, pending_experiments
)

# Construct the partial acquisition function that attaches cont_part
Expand All @@ -157,7 +158,9 @@ def recommend( # noqa: D102
disc_part_tensor = to_tensor(disc_part).unsqueeze(-2)

# Setup a fresh acquisition function for the continuous recommender
self.cont_recommender._setup_botorch_acqf(searchspace, objective, measurements)
self.cont_recommender._setup_botorch_acqf(
searchspace, objective, measurements, pending_experiments
)

# Construct the continuous space as a standalone space
cont_acqf_part = PartialAcquisitionFunction(
Expand Down
20 changes: 19 additions & 1 deletion baybe/recommenders/pure/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,30 @@ class PureRecommender(ABC, RecommenderProtocol):
"""Allow to make recommendations that were measured previously.
This only has an influence in discrete search spaces."""

allow_recommending_pending_experiments: bool = field(default=False, kw_only=True)
"""Allow `pending_experiments` to be part of the recommendations. If set to `False`,
the corresponding points will be removed from the candidates. This only has an
influence in discrete search spaces."""

def recommend( # noqa: D102
self,
batch_size: int,
searchspace: SearchSpace,
objective: Objective | None = None,
measurements: pd.DataFrame | None = None,
pending_experiments: pd.DataFrame | None = None,
) -> pd.DataFrame:
# See base class
if searchspace.type is SearchSpaceType.CONTINUOUS:
return self._recommend_continuous(
subspace_continuous=searchspace.continuous, batch_size=batch_size
)
else:
return self._recommend_with_discrete_parts(searchspace, batch_size)
return self._recommend_with_discrete_parts(
searchspace,
batch_size,
pending_experiments=pending_experiments,
)

def _recommend_discrete(
self,
Expand Down Expand Up @@ -154,6 +164,7 @@ def _recommend_with_discrete_parts(
self,
searchspace: SearchSpace,
batch_size: int,
pending_experiments: pd.DataFrame | None,
) -> pd.DataFrame:
"""Obtain recommendations in search spaces with a discrete part.

Expand All @@ -163,6 +174,7 @@ def _recommend_with_discrete_parts(
Args:
searchspace: The search space from which to generate recommendations.
batch_size: The size of the recommendation batch.
pending_experiments: Pending experiments in experimental representation.

Returns:
A dataframe containing the recommendations as individual rows.
Expand All @@ -175,11 +187,17 @@ def _recommend_with_discrete_parts(

# Get discrete candidates
# Repeated recommendations are always allowed for hybrid spaces
# Pending experiments are excluded for discrete spaces unless configured
# differently.
dont_exclude_pending = (
is_hybrid_space or self.allow_recommending_pending_experiments
)
candidates_exp, _ = searchspace.discrete.get_candidates(
allow_repeated_recommendations=is_hybrid_space
or self.allow_repeated_recommendations,
allow_recommending_already_measured=is_hybrid_space
or self.allow_recommending_already_measured,
exclude=None if dont_exclude_pending else pending_experiments,
)

# TODO: Introduce new flag to recommend batches larger than the search space
Expand Down
13 changes: 11 additions & 2 deletions baybe/recommenders/pure/bayesian/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,16 @@ def _setup_botorch_acqf(
searchspace: SearchSpace,
objective: Objective,
measurements: pd.DataFrame,
pending_experiments: pd.DataFrame | None = None,
) -> None:
"""Create the acquisition function for the current training data.""" # noqa: E501
self.surrogate_model.fit(searchspace, objective, measurements)
self._botorch_acqf = self.acquisition_function.to_botorch(
self.surrogate_model, searchspace, objective, measurements
self.surrogate_model,
searchspace,
objective,
measurements,
pending_experiments,
)

def recommend( # noqa: D102
Expand All @@ -61,6 +66,7 @@ def recommend( # noqa: D102
searchspace: SearchSpace,
objective: Objective | None = None,
measurements: pd.DataFrame | None = None,
pending_experiments: pd.DataFrame | None = None,
) -> pd.DataFrame:
# See base class.

Expand Down Expand Up @@ -89,11 +95,14 @@ def recommend( # noqa: D102
if isinstance(self.surrogate_model, CustomONNXSurrogate):
CustomONNXSurrogate.validate_compatibility(searchspace)

self._setup_botorch_acqf(searchspace, objective, measurements)
self._setup_botorch_acqf(
searchspace, objective, measurements, pending_experiments
)

return super().recommend(
batch_size=batch_size,
searchspace=searchspace,
objective=objective,
measurements=measurements,
pending_experiments=pending_experiments,
)
Loading