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

Continuous inter-point constraints #345

Open
wants to merge 47 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
34ce078
Introduce class for continuous interpoint constraints
AVHopp Oct 22, 2024
fac5953
Add interpoint constraints to search space
AVHopp Oct 22, 2024
ffd6479
Adjust sampling for interpoint constraints
AVHopp Oct 22, 2024
3200258
Use interpoint constraints in BotorchRecommender
AVHopp Oct 22, 2024
9a2d417
Include interpoint constraints in example
AVHopp Oct 22, 2024
95685f3
Include interpoint constraints in tests
AVHopp Oct 22, 2024
37615bb
Simplify definition of inter-point constraints
AVHopp Nov 11, 2024
dec8197
Update CHANGELOG
AVHopp Nov 11, 2024
cf58894
Raise error when using interpoint constraints in hybrid spaces
AVHopp Nov 20, 2024
86e476c
Replace inter-point by interpoint
AVHopp Nov 20, 2024
4fa77a3
Remove duplication of nonlin_constraint_list
AVHopp Nov 20, 2024
d8a9973
Rephrase docstrings of properties
AVHopp Nov 20, 2024
b7ae3e6
Add alias for is_interpoint field
AVHopp Nov 20, 2024
7edbb5b
Improve description of is_interpoint field
AdrianSosic Nov 20, 2024
64eff39
Remove default for batch_size in to_botorch
AVHopp Nov 20, 2024
c367115
Remove batch_size parametrization for interpoint tests
AVHopp Nov 20, 2024
a403948
Create separate interpoint example
AVHopp Nov 20, 2024
31afb21
Add section about interpoint constraints to userguide
AVHopp Nov 20, 2024
eca061c
Add <= interpoint constraint test
AVHopp Nov 20, 2024
e08cd9e
Use coefficients and rhs from to_botorch call
AVHopp Nov 20, 2024
b8e08fb
Unify _sample_from_polytope methods
AVHopp Nov 20, 2024
3927a61
Add explicit assert for mypy
AVHopp Nov 20, 2024
57b0e98
Include motivation for interpoint constraints
AVHopp Nov 22, 2024
f092932
Add comment on relevant constraint to example
AVHopp Nov 22, 2024
881084d
Add TOLERANCE to interpoint tests
AVHopp Nov 22, 2024
ad13835
Fix test mixing normal and interpoint constraint
AVHopp Nov 22, 2024
d98bf90
Prevent using interpoint and cardinality constraints together
AVHopp Dec 16, 2024
bd0c566
Add test for using interpoint and cardinality constraints
AVHopp Dec 16, 2024
e36ee8a
Add admonition on mixing interpoint and cardinality constraints
AVHopp Dec 16, 2024
a1ca1a5
Validate interpoint and cardinality constraints in general call
AVHopp Jan 9, 2025
4656677
Adjust outdated comment
AVHopp Jan 9, 2025
6d13ef5
Base num_of_params on comp_rep instead of parameters
AVHopp Jan 9, 2025
bc11699
Fix capitalization of headings
AVHopp Jan 9, 2025
d168b08
Fix incorrect validation
AVHopp Jan 9, 2025
70a0869
Use fixtures in cardnality and interpoint test
AVHopp Jan 9, 2025
11a1637
Fix incorrect validation
AVHopp Jan 9, 2025
a535005
Fix example
AVHopp Jan 9, 2025
0ca3259
Add atol to test
AVHopp Jan 9, 2025
942dd3b
Use pandas chaining more consistently
AVHopp Jan 9, 2025
bd3afff
Improve code snippet in user guide
AVHopp Jan 9, 2025
4ffa977
Update hypothesis strategy for continuous linear inequalities
AVHopp Jan 9, 2025
b6ee323
Fix example after rebase
AVHopp Jan 9, 2025
80dc507
Fix typo in constraint definition
AVHopp Jan 9, 2025
3b6583b
Replace assert on batch_size by RuntimeError
AVHopp Jan 14, 2025
45675c7
Change name of parameter in userguide
AVHopp Jan 14, 2025
c322b7c
Add comment to remind us of interpoint handling in hypothesis test
AVHopp Jan 14, 2025
2b8d314
Use pandas chaining more consistently
AVHopp Jan 14, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
AVHopp marked this conversation as resolved.
Show resolved Hide resolved
AVHopp marked this conversation as resolved.
Show resolved Hide resolved
Scienfitz marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `arrays_to_dataframes` decorator to create lookups from array-based callables
- `DiscreteConstraint.get_valid` to conveniently access valid candidates
- Functionality for persisting benchmarking results on S3 from a manual pipeline run
- Continuous inter-point constraints via new `is_interpoint` attribute

### Changed
- `SubstanceParameter` encodings are now computed exclusively with the
Expand Down
56 changes: 47 additions & 9 deletions baybe/constraints/continuous.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
import gc
import math
from collections.abc import Collection, Sequence
from itertools import chain, repeat
from typing import TYPE_CHECKING, Any

import numpy as np
from attr.validators import in_
from attr.validators import in_, instance_of
from attrs import define, field

from baybe.constraints.base import (
Expand Down Expand Up @@ -45,6 +46,17 @@ class ContinuousLinearConstraint(ContinuousConstraint):
rhs: float = field(default=0.0, converter=float, validator=finite_float)
"""Right-hand side value of the in-/equality."""

is_interpoint: bool = field(
alias="interpoint", default=False, validator=instance_of(bool)
)
"""Flag for defining an interpoint constraint.

While intra-point constraints impose conditions on each individual point of a batch,
interpoint constraints do so **across** the points of the batch. That is, an
interpoint constraint of the form ``x_1 + x_2 <= 1`` enforces that the sum of all
``x_1`` values plus the sum of all ``x_2`` values in the batch must not exceed 1.
"""

@coefficients.validator
def _validate_coefficients( # noqa: DOC101, DOC103
self, _: Any, coefficients: list[float]
Expand Down Expand Up @@ -98,7 +110,10 @@ def _drop_parameters(
)

def to_botorch(
self, parameters: Sequence[NumericalContinuousParameter], idx_offset: int = 0
self,
parameters: Sequence[NumericalContinuousParameter],
idx_offset: int = 0,
batch_size: int | None = None,
) -> tuple[Tensor, Tensor, float]:
"""Cast the constraint in a format required by botorch.

Expand All @@ -108,25 +123,48 @@ def to_botorch(
Args:
parameters: The parameter objects of the continuous space.
idx_offset: Offset to the provided parameter indices.
batch_size: The batch size used in the recommendation. Necessary for
interpoint constraints, ignored by all others.

Returns:
The tuple required by botorch.

Raises:
RuntimeError: When the constraint is an interpoint constraint but
batch_size is ``None``.
"""
import torch

from baybe.utils.torch import DTypeFloatTorch

param_names = [p.name for p in parameters]
param_indices = [
param_names.index(p) + idx_offset
for p in self.parameters
if p in param_names
]
if not self.is_interpoint:
param_indices = [
param_names.index(p) + idx_offset
for p in self.parameters
if p in param_names
]
coefficients = self.coefficients
torch_indices = torch.tensor(param_indices)
else:
if batch_size is None:
raise RuntimeError(
"No `batch_size` set but using interpoint constraints."
"This should nothappen and means that there is a bug in the code."
)
param_index = {name: param_names.index(name) for name in self.parameters}
param_indices_interpoint = [
(batch, param_index[name] + idx_offset)
for name in self.parameters
for batch in range(batch_size)
]
coefficients = list(chain(*zip(*repeat(self.coefficients, batch_size))))
torch_indices = torch.tensor(param_indices_interpoint)

return (
torch.tensor(param_indices),
torch_indices,
torch.tensor(
[self._multiplier * c for c in self.coefficients], dtype=DTypeFloatTorch
[self._multiplier * c for c in coefficients], dtype=DTypeFloatTorch
),
np.asarray(self._multiplier * self.rhs, dtype=DTypeFloatNumpy).item(),
)
Expand Down
42 changes: 41 additions & 1 deletion baybe/constraints/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
from itertools import combinations

from baybe.constraints.base import Constraint
from baybe.constraints.continuous import ContinuousCardinalityConstraint
from baybe.constraints.continuous import (
ContinuousCardinalityConstraint,
ContinuousConstraint,
ContinuousLinearConstraint,
)
from baybe.constraints.discrete import (
DiscreteDependenciesConstraint,
)
Expand Down Expand Up @@ -36,6 +40,11 @@ def validate_constraints( # noqa: DOC101, DOC103
validate_cardinality_constraints_are_nonoverlapping(
[con for con in constraints if isinstance(con, ContinuousCardinalityConstraint)]
)
validate_no_interpoint_and_cardinality_constraints(
constraints=[
con for con in constraints if isinstance(con, ContinuousConstraint)
]
)

param_names_all = [p.name for p in parameters]
param_names_discrete = [p.name for p in parameters if p.is_discrete]
Expand Down Expand Up @@ -98,3 +107,34 @@ def validate_cardinality_constraints_are_nonoverlapping(
f"cannot share the same parameters. Found the following overlapping "
f"parameter sets: {s1}, {s2}."
)


def validate_no_interpoint_and_cardinality_constraints(
AVHopp marked this conversation as resolved.
Show resolved Hide resolved
constraints: Collection[ContinuousConstraint],
):
"""Validate that cardinality and interpoint constraints are not used together.

This is a current limitation in our code and might be enabled in the future.

Args:
constraints: A collection of continuous constraints.

Raises:
ValueError: If there are both interpoint and cardinality constraints.
"""
# Check is a bit cumbersome since the is_interpoint field is currently defined
# for ContinouosLinearConstraint only as these are the only ones that can
# actually be interpoint.
has_interpoint = any(
c.is_interpoint
for c in constraints
if isinstance(c, ContinuousLinearConstraint)
)
has_cardinality = any(
isinstance(c, ContinuousCardinalityConstraint) for c in constraints
)
if has_interpoint and has_cardinality:
raise ValueError(
f"Cconstraints of type `{ContinuousCardinalityConstraint.__name__}` "
"cannot be used together with interpoint constraints."
)
9 changes: 7 additions & 2 deletions baybe/recommenders/pure/bayesian/botorch.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,12 @@ def _recommend_continuous(
num_restarts=self.n_restarts,
raw_samples=self.n_raw_samples,
equality_constraints=[
c.to_botorch(subspace_continuous.parameters)
c.to_botorch(subspace_continuous.parameters, batch_size=batch_size)
for c in subspace_continuous.constraints_lin_eq
]
or None, # TODO: https://github.com/pytorch/botorch/issues/2042
inequality_constraints=[
c.to_botorch(subspace_continuous.parameters)
c.to_botorch(subspace_continuous.parameters, batch_size=batch_size)
for c in subspace_continuous.constraints_lin_ineq
]
or None, # TODO: https://github.com/pytorch/botorch/issues/2042
Expand Down Expand Up @@ -234,6 +234,11 @@ def _recommend_hybrid(
Returns:
The recommended points.
"""
if searchspace.continuous.has_interpoint_constraints:
raise NotImplementedError(
"Interpoint constraints are not available in hybrid spaces."
AdrianSosic marked this conversation as resolved.
Show resolved Hide resolved
)

# For batch size > 1, this optimizer needs a MC acquisition function
if batch_size > 1 and not self.acquisition_function.is_mc:
raise IncompatibleAcquisitionFunctionError(
Expand Down
96 changes: 81 additions & 15 deletions baybe/searchspace/continuous.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from baybe.constraints.base import ContinuousConstraint, ContinuousNonlinearConstraint
from baybe.constraints.validation import (
validate_cardinality_constraints_are_nonoverlapping,
validate_no_interpoint_and_cardinality_constraints,
)
from baybe.parameters import NumericalContinuousParameter
from baybe.parameters.base import ContinuousParameter
Expand Down Expand Up @@ -83,6 +84,7 @@ def __str__(self) -> str:
nonlin_constraints_list = [
constr.summary() for constr in self.constraints_nonlin
]

param_df = pd.DataFrame(param_list)
lin_eq_df = pd.DataFrame(eq_constraints_list)
lin_ineq_df = pd.DataFrame(ineq_constraints_list)
Expand Down Expand Up @@ -173,6 +175,7 @@ def from_product(
) -> SubspaceContinuous:
"""See :class:`baybe.searchspace.core.SearchSpace`."""
constraints = constraints or []
validate_no_interpoint_and_cardinality_constraints(constraints)
return SubspaceContinuous(
parameters=[p for p in parameters if p.is_continuous], # type:ignore[misc]
constraints_lin_eq=[ # type:ignore[attr-misc]
Expand Down Expand Up @@ -286,6 +289,24 @@ def comp_rep_bounds(self) -> pd.DataFrame:
dtype=DTypeFloatNumpy,
)

@property
def is_constrained(self) -> bool:
"""Boolean flag indicating whether the subspace is constrained in any way."""
return any(
(
self.constraints_lin_eq,
self.constraints_lin_ineq,
self.constraints_nonlin,
)
)

@property
def has_interpoint_constraints(self) -> bool:
"""Boolean flag indicating whether the space has any interpoint constraints."""
return any(
c.is_interpoint for c in self.constraints_lin_eq + self.constraints_lin_ineq
)

def _drop_parameters(self, parameter_names: Collection[str]) -> SubspaceContinuous:
"""Create a copy of the subspace with certain parameters removed.

Expand Down Expand Up @@ -391,14 +412,11 @@ def sample_uniform(self, batch_size: int = 1) -> pd.DataFrame:

if not self.parameters:
return pd.DataFrame(index=pd.RangeIndex(0, batch_size))

if (
len(self.constraints_lin_eq) == 0
and len(self.constraints_lin_ineq) == 0
and len(self.constraints_cardinality) == 0
):
# If the space is completely unconstrained, we can sample from bounds.
if not self.is_constrained:
return self._sample_from_bounds(batch_size, self.comp_rep_bounds.values)

# If there are no cardinality constraints, we sample directly from the polytope
if len(self.constraints_cardinality) == 0:
return self._sample_from_polytope(batch_size, self.comp_rep_bounds.values)

Expand All @@ -413,22 +431,70 @@ def _sample_from_bounds(self, batch_size: int, bounds: np.ndarray) -> pd.DataFra
return pd.DataFrame(points, columns=self.parameter_names)

def _sample_from_polytope(
self, batch_size: int, bounds: np.ndarray
self,
batch_size: int,
bounds: np.ndarray,
) -> pd.DataFrame:
"""Draw uniform random samples from a polytope."""
# If the space has interpoint constraints, we need to sample from a larger
# searchspace that models the batch size via additional dimension. This is
# necessary since `get_polytope_samples` cannot handle interpoint constraints,
# see https://github.com/pytorch/botorch/issues/2468

import torch
from botorch.utils.sampling import get_polytope_samples

# The number of parameters is needed at some places for adjusting indices
num_of_params = len(self.comp_rep_columns)

eq_constraints, ineq_constraints = [], []

for c in [*self.constraints_lin_eq, *self.constraints_lin_ineq]:
if not c.is_interpoint:
param_indices, coefficients, rhs = c.to_botorch(self.parameters)
for b in range(batch_size):
botorch_tuple = (
param_indices + b * num_of_params,
coefficients,
rhs,
)
if c.is_eq:
eq_constraints.append(botorch_tuple)
else:
ineq_constraints.append(botorch_tuple)
else:
# Get the indices of the parameters used in the constraint
param_index = {
name: self.parameter_names.index(name) for name in c.parameters
}
param_indices_list = [
batch * num_of_params + param_index[param]
for param in c.parameters
for batch in range(batch_size)
]
_, coefficients, rhs = c.to_botorch(
parameters=self.parameters, batch_size=batch_size
)
botorch_tuple = (
torch.tensor(param_indices_list),
coefficients,
rhs,
)
if c.is_eq:
eq_constraints.append(botorch_tuple)
else:
ineq_constraints.append(botorch_tuple)

bounds_joint = torch.cat(
[torch.from_numpy(bounds) for _ in range(batch_size)], dim=-1
)
points = get_polytope_samples(
n=batch_size,
bounds=torch.from_numpy(bounds),
equality_constraints=[
c.to_botorch(self.parameters) for c in self.constraints_lin_eq
],
inequality_constraints=[
c.to_botorch(self.parameters) for c in self.constraints_lin_ineq
],
n=1,
bounds=bounds_joint,
equality_constraints=eq_constraints,
inequality_constraints=ineq_constraints,
)
points = points.reshape(batch_size, points.shape[-1] // batch_size)
return pd.DataFrame(points, columns=self.parameter_names)

def _sample_from_polytope_with_cardinality_constraints(
Expand Down
32 changes: 32 additions & 0 deletions docs/userguide/constraints.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,38 @@ ContinuousLinearConstraint(
A more detailed example can be found
[here](../../examples/Constraints_Continuous/linear_constraints).

### Interpoint Constraints

The constraints discussed so far all belong to the class of so called "intrapoint constraints".
That is, they impose conditions on each individual point of a batch.
In contrast to this, interpoint constraints do so **across** the points of the batch.
That is, an interpoint constraint of the form ``x_1 + x_2 <= 1`` enforces that the sum of all
``x_1`` values plus the sum of all ``x_2`` values in the batch must not exceed 1.
A possible relevant constraint might be that only 100ml of a given solvent are available for
a full batch, but there is no limit for the amount of solvent to use for a single experiment
within that batch.

They can be defined by using the `interpoint` keyword of the [`ContinuousLinearConstraint`](baybe.constraints.continuous.ContinuousLinearConstraint)
class.
```python
from baybe.constraints import ContinuousLinearConstraint

ContinuousLinearConstraint(
parameters=["SolventAmount[ml]"],
operator="<=",
coefficients=[1.0],
rhs=100,
interpoint=True,
)

```

```{admonition} Mixing Interpoint and Cardinality Constraints
:class: note
Currently, BayBE does not support to use both interpoint and cardinality constraints
within the same search space.
```

## Conditions
Conditions are elements used within discrete constraints.
While discrete constraints can operate on one or multiple parameters, a condition
Expand Down
Loading
Loading