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

Botorch with cardinality constraint via sampling #301

Draft
wants to merge 69 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
d811240
Enable cardinality constraint in botorch recommender via sampling ina…
Waschenbacher Jul 2, 2024
da813f5
Make inactive parameters fixed features
Waschenbacher Jul 2, 2024
adf5cc2
Fix bug in test file
Waschenbacher Jul 4, 2024
e69ceff
Validate bounds of cardinality constraint parameters
Waschenbacher Jul 2, 2024
2270350
Add second option: iterate through combinatorial list
Waschenbacher Jul 3, 2024
6483e4b
Fix type error
Waschenbacher Jul 3, 2024
ae919d4
Revise botorch+cardinality constraint for enhanced clarity
Waschenbacher Jul 4, 2024
9ab8fda
Fix property names and its docstrings
Waschenbacher Jul 11, 2024
c3831c7
Use guard clause
Waschenbacher Jul 11, 2024
2f49f5a
Simplify syntax with 'prod'
Waschenbacher Jul 11, 2024
d46fd60
Refactor botorch+cardinality constraint
Waschenbacher Jul 12, 2024
293e2ef
Make 'n_threshold_inactive_parameters_generator' an attribute of boto…
Waschenbacher Jul 12, 2024
f8d0713
Refactor combinatorial properties of cardinality constraint
AdrianSosic Aug 15, 2024
76b5d72
Refactor combinatorial properties of continuous subspace
AdrianSosic Aug 15, 2024
5c92079
Refactor constraint validation
AdrianSosic Aug 15, 2024
f07a452
Move factory code up
AdrianSosic Aug 15, 2024
c5b014d
Simplify constructor code
AdrianSosic Aug 15, 2024
306c9d2
Update CHANGELOG.md
AdrianSosic Aug 15, 2024
7687e39
Ensure active parameters by altering parameters bounds
Waschenbacher Aug 23, 2024
66c3278
Fix continuous constraint test
Waschenbacher Aug 23, 2024
a1d11e7
Refactor botorch interface using fixed parameter class
Waschenbacher Aug 23, 2024
22fd942
Add try-except block to handle infeasible problem at certain inactive…
Waschenbacher Aug 26, 2024
120d717
Update CHANGELOG.md
Waschenbacher Aug 26, 2024
e6248b5
Fix type hint
Waschenbacher Aug 26, 2024
e0508b9
Fix test by repacing match text
Waschenbacher Aug 26, 2024
04d89d1
Refine docstrings
AdrianSosic Oct 15, 2024
102ef07
Fix method return type
AdrianSosic Oct 15, 2024
e357c95
Merge branch 'main' into feature/cardinality_constraint_to_botorch_vi…
AdrianSosic Oct 25, 2024
3d72f04
Fix capitalization in exception group
AdrianSosic Oct 25, 2024
ed8054b
Add explicit error handling to validator
AdrianSosic Oct 25, 2024
756bb09
Clean up cardinality constraint helper property
AdrianSosic Oct 25, 2024
b0c422e
Refactor parameter activation logic
AdrianSosic Oct 25, 2024
ddabcf5
Refactor method for enforcing cardinality constraints
AdrianSosic Oct 25, 2024
b8d24d7
Fix exception types and messages
AdrianSosic Oct 28, 2024
492bb3b
Apply minor formatting and documentation fixes
AdrianSosic Oct 28, 2024
584d8d9
Remove unnecessary `len` call
AdrianSosic Oct 28, 2024
5744e31
Remove unnecessary function layer
AdrianSosic Oct 28, 2024
e4afdcc
Extract loop into general function optimizing subspaces
AdrianSosic Oct 28, 2024
788a5ba
Simplify multi-space optimization logic
AdrianSosic Oct 29, 2024
046a8e2
Remove restriction on subspaces without cardinality constraints
AdrianSosic Oct 29, 2024
7aac4d3
Move __str__ method to top
AdrianSosic Oct 29, 2024
3c829d0
Rename threshold attribute
AdrianSosic Oct 29, 2024
bc697c0
Add item to README.md
AdrianSosic Oct 30, 2024
b9038b8
Implement summary method
AdrianSosic Oct 30, 2024
c2c8b99
Update CHANGELOG.md
AdrianSosic Nov 1, 2024
a721f21
Fix tests
AdrianSosic Nov 1, 2024
e84dda9
Explain mechanism of recommending with cardinality constraints
AdrianSosic Nov 1, 2024
fa13267
Add near-zero threshold to continuous numerical parameter
Waschenbacher Dec 13, 2024
7aa7c3f
Refine activate parameter helper function
Waschenbacher Dec 13, 2024
cfdf1e3
Show warnings when any minimum cardinality constraints are violated.
Waschenbacher Dec 14, 2024
e3c6620
Update test related to cardinality constraints
Waschenbacher Dec 15, 2024
b85924f
Add to-dos
Waschenbacher Dec 15, 2024
22b19f9
Add test on catching warning related to violation of minimum cardinal…
Waschenbacher Dec 16, 2024
b0dc037
Update CHANGELOG.md
Waschenbacher Dec 16, 2024
3fa8b02
Merge branch 'main' into feature/cardinality_constraint_to_botorch_vi…
Waschenbacher Dec 16, 2024
35825b8
Clean up merge conflict code
Waschenbacher Dec 16, 2024
3e275e4
Refine logic in counting the near-zero elements
Waschenbacher Dec 16, 2024
62f0ed6
Add TODO related to customized infeasibility error in botorch
Waschenbacher Jan 8, 2025
9af846b
Add threshold to continuous cardinality constraint
Waschenbacher Jan 8, 2025
10e0812
Adapt activate_parameter towards threshold per cardinality constraints
Waschenbacher Jan 8, 2025
142b1ec
Refine check cardinaltiy constraint fulfillment logic
Waschenbacher Jan 8, 2025
04f145c
Remove threshold related attribute and method in numerical continuous…
Waschenbacher Jan 9, 2025
55a7ba3
Make zero-checking and threshold definition compatible
Waschenbacher Jan 9, 2025
78b115f
Add activate parameter step in random sampler
Waschenbacher Jan 9, 2025
68045a7
Update CHANGELOG.md
Waschenbacher Jan 9, 2025
e6e2e97
Fix type hint in continuous numerical parameter classes
Waschenbacher Jan 9, 2025
a30b009
Test activate parameter function
Waschenbacher Jan 13, 2025
983b1a9
Correct logic on boundary handling in activate paramter
Waschenbacher Jan 13, 2025
bddab62
Ensure parameter bounds cover zero
Waschenbacher 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
10 changes: 10 additions & 0 deletions CHANGELOG.md
AVHopp marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
– `Campaign.toggle_discrete_candidates` to dynamically in-/exclude discrete candidates
- `DiscreteConstraint.get_valid` to conveniently access valid candidates
- Functionality for persisting benchmarking results on S3 from a manual pipeline run
- `ContinuousCardinalityConstraint` is now compatible with `BotorchRecommender`
- Warning `MinimumCardinalityViolatedWarning` is triggered when any minimum
cardinality is violated in `BotorchRecommender`
- Attribute `max_n_subspaces` to `BotorchRecommender`, allowing to control
optimization behavior in the presence of multiple subspaces
- Utilities `inactive_parameter_combinations` and`n_inactive_parameter_combinations`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these as well as the utilities noted below are not user-facing, are they? In that case, I do not think that it is necessary to include them in the CHANGELOG

in both `ContinuousCardinalityConstraint`and `SubspaceContinuous`
- Attribute `near_zero_threshold` and utility `is_near_zero` to
`NumericalContinuousParameter`
- Utilities `count_near_zeros` and `is_min_cardinality_fulfilled`

### Changed
- `SubstanceParameter` encodings are now computed exclusively with the
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Besides functionality to perform a typical recommend-measure loop, BayBE's highl
- 🎭 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)
- 🔢 Cardinality constraints: Control the number of active factors in your design
- 🌎 Distributed workflows: Run campaigns asynchronously with pending experiments
- 🎓 Active learning: Perform smart data acquisition campaigns
- ⚙️ Custom surrogate models: Enhance your predictions through mechanistic understanding
Expand Down
24 changes: 23 additions & 1 deletion baybe/constraints/continuous.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

import gc
import math
from collections.abc import Collection, Sequence
from collections.abc import Collection, Iterator, Sequence
from itertools import combinations
from math import comb
from typing import TYPE_CHECKING, Any

import numpy as np
Expand Down Expand Up @@ -138,6 +140,26 @@ class ContinuousCardinalityConstraint(
):
"""Class for continuous cardinality constraints."""

@property
def n_inactive_parameter_combinations(self) -> int:
"""The number of possible inactive parameter combinations."""
Scienfitz marked this conversation as resolved.
Show resolved Hide resolved
return sum(
AVHopp marked this conversation as resolved.
Show resolved Hide resolved
comb(len(self.parameters), n_inactive_parameters)
for n_inactive_parameters in self._inactive_set_sizes()
)

def _inactive_set_sizes(self) -> range:
AdrianSosic marked this conversation as resolved.
Show resolved Hide resolved
"""Get all possible sizes of inactive parameter sets."""
return range(
len(self.parameters) - self.max_cardinality,
len(self.parameters) - self.min_cardinality + 1,
)

def inactive_parameter_combinations(self) -> Iterator[frozenset[str]]:
"""Get an iterator over all possible combinations of inactive parameters."""
for n_inactive_parameters in self._inactive_set_sizes():
yield from combinations(self.parameters, n_inactive_parameters)

def sample_inactive_parameters(self, batch_size: int = 1) -> list[set[str]]:
"""Sample sets of inactive parameters according to the cardinality constraints.

Expand Down
57 changes: 57 additions & 0 deletions baybe/constraints/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@
from baybe.constraints.discrete import (
DiscreteDependenciesConstraint,
)
from baybe.parameters import NumericalContinuousParameter
from baybe.parameters.base import Parameter

try: # For python < 3.11, use the exceptiongroup backport
ExceptionGroup
except NameError:
from exceptiongroup import ExceptionGroup


def validate_constraints( # noqa: DOC101, DOC103
constraints: Collection[Constraint], parameters: Collection[Parameter]
Expand All @@ -26,6 +32,8 @@ def validate_constraints( # noqa: DOC101, DOC103
ValueError: If any discrete constraint includes a continuous parameter.
ValueError: If any discrete constraint that is valid only for numerical
discrete parameters includes non-numerical discrete parameters.
ValueError: If any parameter affected by a cardinality constraint does
AVHopp marked this conversation as resolved.
Show resolved Hide resolved
not include zero.
"""
if sum(isinstance(itm, DiscreteDependenciesConstraint) for itm in constraints) > 1:
raise ValueError(
Expand All @@ -41,6 +49,9 @@ def validate_constraints( # noqa: DOC101, DOC103
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]
param_names_non_numerical = [p.name for p in parameters if not p.is_numerical]
params_continuous: list[NumericalContinuousParameter] = [
p for p in parameters if isinstance(p, NumericalContinuousParameter)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would reutilzie the list param_names_continuous from above

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if that's too helpful, since it contains only the names. Or what exactly do you suggest?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.get_parameters_by_name

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AdrianSosic I didn't find the method mentioned above. In general .get_parameters_by_name can be really handy at several places. Let me know pls, if it is already implemented somewhere or if it should be added in this PR (I can add it).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

@Waschenbacher Waschenbacher Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Scienfitz The PR above has been merged. I used.get_parameters_by_name somewhere else, but not here. It is a method of SubspaceContinuous and we do not have the subspace_continuous object here. In addition, we need the type NumericalContinuousParameter inparams_continuous. I prefer to keep it in this way unless there is a strong opinion against it.

]

for constraint in constraints:
if not all(p in param_names_all for p in constraint.parameters):
Expand Down Expand Up @@ -78,6 +89,11 @@ def validate_constraints( # noqa: DOC101, DOC103
f"Parameter list of the affected constraint: {constraint.parameters}."
)

if isinstance(constraint, ContinuousCardinalityConstraint):
validate_cardinality_constraint_parameter_bounds(
constraint, params_continuous
)


def validate_cardinality_constraints_are_nonoverlapping(
constraints: Collection[ContinuousCardinalityConstraint],
Expand All @@ -98,3 +114,44 @@ def validate_cardinality_constraints_are_nonoverlapping(
f"cannot share the same parameters. Found the following overlapping "
f"parameter sets: {s1}, {s2}."
)


def validate_cardinality_constraint_parameter_bounds(
constraint: ContinuousCardinalityConstraint,
parameters: Collection[NumericalContinuousParameter],
) -> None:
"""Validate that all parameters of a continuous cardinality constraint include zero.

Args:
constraint: A continuous cardinality constraint.
parameters: A collection of parameters, including those affected by the
constraint.

Raises:
ValueError: If one of the affected parameters does not include zero.
ExceptionGroup: If several of the affected parameters do not include zero.
AVHopp marked this conversation as resolved.
Show resolved Hide resolved
"""
exceptions = []
for name in constraint.parameters:
try:
parameter = next(p for p in parameters if p.name == name)
except StopIteration as ex:
raise ValueError(
f"The parameter '{name}' referenced by the constraint is not contained "
f"in the given collection of parameters."
) from ex

if not parameter.is_in_range(0.0):
exceptions.append(
ValueError(
f"The bounds of all parameters affected by a constraint of type "
f"'{ContinuousCardinalityConstraint.__name__}' must include zero, "
f"but the bounds of parameter '{name}' are: "
f"{parameter.bounds.to_tuple()}"
)
)

if exceptions:
if len(exceptions) == 1:
raise exceptions[0]
raise ExceptionGroup("Invalid parameter bounds", exceptions)
4 changes: 4 additions & 0 deletions baybe/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ class UnusedObjectWarning(UserWarning):
"""


class MinimumCardinalityViolatedWarning(UserWarning):
"""Minimum cardinality constraints are violated."""


##### Exceptions #####


Expand Down
51 changes: 51 additions & 0 deletions baybe/parameters/numerical.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ class NumericalContinuousParameter(ContinuousParameter):
bounds: Interval = field(default=None, converter=convert_bounds)
"""The bounds of the parameter."""

near_zero_threshold: float = field(default=1e-5, converter=float)
"""A threshold for determining if the value is considered near-zero."""

@bounds.validator
def _validate_bounds(self, _: Any, value: Interval) -> None: # noqa: DOC101, DOC103
"""Validate bounds.
Expand Down Expand Up @@ -149,6 +152,54 @@ def summary(self) -> dict:
)
return param_dict

def is_near_zero(self, item: float) -> bool:
"""Return whether an item is near-zero.
Waschenbacher marked this conversation as resolved.
Show resolved Hide resolved

Important:
Value in the open interval (-near_zero_threshold, near_zero_threshold)
will be treated as near_zero.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Insonsistencies regarding the use of near_zero and near-zero in this docstring.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is renamed to zeros in the updated version. Resolve it for now. If near_zero is preferred, I'm open to rename it back.


Args:
item: The value to be checked.

Returns:
``True`` if the value is near-zero, ``False`` otherwise.
"""
return abs(item) < self.near_zero_threshold


@define(frozen=True, slots=False)
class _FixedNumericalContinuousParameter(ContinuousParameter):
AdrianSosic marked this conversation as resolved.
Show resolved Hide resolved
"""Parameter class for fixed numerical parameters."""

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

value: float = field(converter=float)
"""The fixed value of the parameter."""

@property
def bounds(self) -> Interval:
"""The value of the parameter as a degenerate interval."""
return Interval(self.value, self.value)

@override
def is_in_range(self, item: float) -> bool:
return item == self.value
AVHopp marked this conversation as resolved.
Show resolved Hide resolved

@override
@property
def comp_rep_columns(self) -> tuple[str, ...]:
AdrianSosic marked this conversation as resolved.
Show resolved Hide resolved
return (self.name,)

@override
def summary(self) -> dict:
return dict(
Name=self.name,
Type=self.__class__.__name__,
Value=self.value,
)


# Collect leftover original slotted classes processed by `attrs.define`
gc.collect()
47 changes: 47 additions & 0 deletions baybe/parameters/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
from typing import Any, TypeVar

import pandas as pd
from attrs import evolve

from baybe.parameters.base import Parameter
from baybe.parameters.numerical import NumericalContinuousParameter

_TParameter = TypeVar("_TParameter", bound=Parameter)

Expand Down Expand Up @@ -87,3 +89,48 @@ def get_parameters_from_dataframe(
def sort_parameters(parameters: Collection[Parameter]) -> tuple[Parameter, ...]:
"""Sort parameters alphabetically by their names."""
return tuple(sorted(parameters, key=lambda p: p.name))


def activate_parameter(
Waschenbacher marked this conversation as resolved.
Show resolved Hide resolved
AdrianSosic marked this conversation as resolved.
Show resolved Hide resolved
parameter: NumericalContinuousParameter,
) -> NumericalContinuousParameter:
"""Activates a given parameter by moving its bounds away from zero.

Important:
Waschenbacher marked this conversation as resolved.
Show resolved Hide resolved
Parameters whose ranges include zero but whose bounds do not overlap with the
inactive range (i.e. parameters that contain the value zero far from their
boundary values) remain unchanged, because the corresponding activated parameter
would no longer have a continuous value range.

Args:
parameter: The parameter to be activated.

Returns:
A copy of the parameter with adjusted bounds.

Raises:
ValueError: If the parameter cannot be activated since both its bounds are
in the inactive range.
"""
lower = parameter.bounds.lower
upper = parameter.bounds.upper

# Upper bound is in near-zero range
if lower <= -parameter.near_zero_threshold and parameter.is_near_zero(upper):
return evolve(parameter, bounds=(lower, -parameter.near_zero_threshold))

# Lower bound is in near-zero range
if upper > parameter.near_zero_threshold and parameter.is_near_zero(lower):
return evolve(parameter, bounds=(parameter.near_zero_threshold, upper))

# Both bounds in inactive range
if parameter.is_near_zero(lower) and parameter.is_near_zero(upper):
raise ValueError(
f"Parameter '{parameter.name}' cannot be set active since its "
f"bounds {parameter.bounds.to_tuple()} are entirely contained in the "
f"inactive range [-{parameter.near_zero_threshold},"
f" {parameter.near_zero_threshold}]."
)

# Both bounds separated from inactive range
return parameter
Loading
Loading