Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `identify_non_dominated_configurations` method to `Campaign` and `Objective`
for determining the Pareto front
- Interpoint constraints for continuous search spaces
- `DiscreteBatchConstraint` for ensuring all recommendations in a batch share
the same value for a specified discrete parameter

### Breaking Changes
- `ContinuousLinearConstraint.to_botorch` now returns a collection of constraint tuples
instead of a single tuple (needed for interpoint constraints)

### Fixed
- `ContinuousCardinalityConstraint` now works in hybrid search spaces
- Typo in `_FixedNumericalContinuousParameter` where `is_numeric` was used
instead of `is_numerical`
- `SHAPInsight` breaking with `numpy>=2.4` due to no longer accepted implicit array to
scalar conversion

Expand All @@ -27,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
can now be conveniently controlled via the new `Settings` mechanism

### Deprecations
- `BotorchRecommender.max_n_subspaces` has been renamed to `max_n_partitions`
- `set_random_seed` and `temporary_seed` utility functions
- The environment variables
`BAYBE_NUMPY_USE_SINGLE_PRECISION`/`BAYBE_TORCH_USE_SINGLE_PRECISION` have been
Expand Down
2 changes: 2 additions & 0 deletions baybe/constraints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from baybe.constraints.discrete import (
DISCRETE_CONSTRAINTS_FILTERING_ORDER,
DiscreteBatchConstraint,
DiscreteCardinalityConstraint,
DiscreteCustomConstraint,
DiscreteDependenciesConstraint,
Expand All @@ -33,6 +34,7 @@
"ContinuousLinearEqualityConstraint",
"ContinuousLinearInequalityConstraint",
# --- Discrete constraints ---#
"DiscreteBatchConstraint",
"DiscreteCardinalityConstraint",
"DiscreteCustomConstraint",
"DiscreteDependenciesConstraint",
Expand Down
73 changes: 73 additions & 0 deletions baybe/constraints/discrete.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from functools import reduce
from typing import TYPE_CHECKING, Any, ClassVar, cast

import numpy as np
import numpy.typing as npt
import pandas as pd
from attrs import define, field
from attrs.validators import in_, min_len
Expand Down Expand Up @@ -355,6 +357,77 @@ def get_invalid(self, data: pd.DataFrame) -> pd.Index:
return data.index[mask_bad]


@define
class DiscreteBatchConstraint(DiscreteConstraint):
"""Constraint ensuring all batch recommendations share the same parameter value.

When this constraint is active, the recommender internally partitions the
candidate set into partitions — one for each unique value of the constrained
parameter — obtains a full batch recommendation from each partition, and
returns the batch with the highest joint acquisition value.

This constraint is not supported by all recommenders. It is not applied during
search space creation (all parameter values remain in the search space).

Example:
If parameter ``Temperature`` has values ``[50, 100, 150]`` and a batch of
10 is requested, the recommender will generate three candidate batches
(one all-50, one all-100, one all-150) and return the best one.
"""

# Class variables
eval_during_creation: ClassVar[bool] = False
eval_during_modeling: ClassVar[bool] = True

numerical_only: ClassVar[bool] = False
# See base class.

def __attrs_post_init__(self):
"""Validate that exactly one parameter is specified."""
if len(self.parameters) != 1:
raise ValueError(
f"'{self.__class__.__name__}' requires exactly one parameter, "
f"but {len(self.parameters)} were provided: {self.parameters}."
)

@override
def get_invalid(self, data: pd.DataFrame) -> pd.Index:
"""Get the indices of invalid rows.

Always returns an empty index because this constraint operates at the
batch level, not the row level. Individual rows are never invalid; the
constraint is enforced at recommendation time by partitioning candidates
into partitions.

Args:
data: A dataframe where each row represents a parameter configuration.

Returns:
An empty index.
"""
return pd.Index([])

def partition_masks(
self, candidates_exp: pd.DataFrame
) -> list[npt.NDArray[np.bool_]]:
"""Return boolean masks defining the partitions for this constraint.

Each mask selects the rows in ``candidates_exp`` that belong to one
partition, i.e. share the same value for the constrained parameter.

Args:
candidates_exp: The experimental representation of candidate points.

Returns:
A list of boolean masks, one per unique value of the constrained
parameter.
"""
param = self.parameters[0]
return [
(candidates_exp[param] == v).values for v in candidates_exp[param].unique()
]


@define
class DiscreteCardinalityConstraint(CardinalityConstraint, DiscreteConstraint):
"""Class for discrete cardinality constraints."""
Expand Down
12 changes: 12 additions & 0 deletions baybe/constraints/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from baybe.constraints.base import Constraint
from baybe.constraints.continuous import ContinuousCardinalityConstraint
from baybe.constraints.discrete import (
DiscreteBatchConstraint,
DiscreteDependenciesConstraint,
)
from baybe.parameters import NumericalContinuousParameter
Expand All @@ -27,6 +28,7 @@ def validate_constraints( # noqa: DOC101, DOC103
:class:`baybe.constraints.discrete.DiscreteDependenciesConstraint` declared.
ValueError: If any two continuous cardinality constraints have an overlapping
parameter set.
ValueError: If multiple batch constraints reference the same parameter.
ValueError: If any constraint contains an invalid parameter name.
ValueError: If any continuous constraint includes a discrete parameter.
ValueError: If any discrete constraint includes a continuous parameter.
Expand All @@ -45,6 +47,16 @@ def validate_constraints( # noqa: DOC101, DOC103
[con for con in constraints if isinstance(con, ContinuousCardinalityConstraint)]
)

batch_param_names = [
c.parameters[0] for c in constraints if isinstance(c, DiscreteBatchConstraint)
]
if duplicates := {n for n in batch_param_names if batch_param_names.count(n) > 1}:
raise ValueError(
f"Multiple '{DiscreteBatchConstraint.__name__}' instances reference "
f"the same parameter(s): {duplicates}. Each parameter can have at "
f"most one batch constraint."
)

param_names_all = [p.name for p in parameters]
param_names_discrete = [p.name for p in parameters if p.is_discrete]
param_names_continuous = [p.name for p in parameters if p.is_continuous]
Expand Down
2 changes: 1 addition & 1 deletion baybe/parameters/numerical.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def summary(self) -> dict:
class _FixedNumericalContinuousParameter(ContinuousParameter):
"""Parameter class for fixed numerical parameters."""

is_numeric: ClassVar[bool] = True
is_numerical: ClassVar[bool] = True
# See base class.

value: float = field(converter=float)
Expand Down
4 changes: 2 additions & 2 deletions baybe/recommenders/naive.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ class NaiveHybridSpaceRecommender(PureRecommender):
# problem that might come up when implementing new subclasses of PureRecommender
disc_recommender: PureRecommender = field(factory=BotorchRecommender)
"""The recommender used for the discrete subspace. Default:
:class:`baybe.recommenders.pure.bayesian.botorch.BotorchRecommender`"""
:class:`baybe.recommenders.pure.bayesian.botorch.core.BotorchRecommender`"""

cont_recommender: BayesianRecommender = field(factory=BotorchRecommender)
"""The recommender used for the continuous subspace. Default:
:class:`baybe.recommenders.pure.bayesian.botorch.BotorchRecommender`"""
:class:`baybe.recommenders.pure.bayesian.botorch.core.BotorchRecommender`"""

@override
def recommend(
Expand Down
24 changes: 23 additions & 1 deletion baybe/recommenders/pure/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
from cattrs.gen import make_dict_unstructure_fn
from typing_extensions import override

from baybe.exceptions import DeprecationError, NotEnoughPointsLeftError
from baybe.exceptions import (
DeprecationError,
IncompatibilityError,
NotEnoughPointsLeftError,
)
from baybe.objectives.base import Objective
from baybe.recommenders.base import RecommenderProtocol
from baybe.searchspace import SearchSpace
Expand All @@ -38,6 +42,10 @@ class PureRecommender(ABC, RecommenderProtocol):
compatibility: ClassVar[SearchSpaceType]
"""Class variable reflecting the search space compatibility."""

supports_discrete_batch_constraints: ClassVar[bool] = False
"""Class variable indicating whether the recommender supports discrete
batch constraints."""

_deprecated_allow_repeated_recommendations: bool = field(
alias="allow_repeated_recommendations",
default=None,
Expand Down Expand Up @@ -259,6 +267,20 @@ def _recommend_with_discrete_parts(
"""
is_hybrid_space = searchspace.type is SearchSpaceType.HYBRID

# Check batch constraint support
if (
searchspace.discrete.constraints_batch
and not self.supports_discrete_batch_constraints
):
constraint_types = {
type(c).__name__ for c in searchspace.discrete.constraints_batch
}
raise IncompatibilityError(
f"'{self.__class__.__name__}' does not support discrete "
f"batch constraints. The search space contains: "
f"{constraint_types}."
)

# Get discrete candidates
candidates_exp, _ = searchspace.discrete.get_candidates()

Expand Down
Loading
Loading