Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
030a78f
Drop SubspaceDiscrete.empty_encoding attribute
AdrianSosic Apr 7, 2026
2f41bec
Drop comp_rep parameter from SubspaceDiscrete.__init__
AdrianSosic Apr 7, 2026
985a519
Fix SubspaceDiscrete class and attribute docstrings
AdrianSosic Apr 7, 2026
8cd624f
Add missing validator to SubspaceDiscrete.parameters
AdrianSosic Apr 7, 2026
b99c0b1
Clean up SubspaceContinuous attributes
AdrianSosic Apr 7, 2026
6882273
Fix SubspaceContinuous class and attribute docstrings
AdrianSosic Apr 7, 2026
e1555d3
Use SubspaceContinuous.__init__ instead of from_product
AdrianSosic Apr 7, 2026
42029b6
Add TODO note to SubspaceDiscrete
AdrianSosic Apr 7, 2026
1db9400
Add deprecation mechanism for legacy constraint arguments
AdrianSosic Apr 7, 2026
2d1be3a
User custom __init__ instead of deprecation attributes
AdrianSosic Apr 7, 2026
d4776bc
Add support for structuring from legacy arguments
AdrianSosic Apr 7, 2026
89c9262
Add deprecation mechanism for legacy SubspaceDiscrete arguments
AdrianSosic Apr 7, 2026
789dfce
Raise warning instead of error for empty_encoding
AdrianSosic Apr 7, 2026
eac2a96
Update CHANGELOG.md
AdrianSosic Apr 7, 2026
39b4f30
Drop dead code
AdrianSosic Apr 7, 2026
a1b23c6
Add missing test for toggled candidates recommendation
AdrianSosic Apr 7, 2026
3b9cc5e
Add deserialization test for legacy SubspaceDiscrete format
AdrianSosic Apr 7, 2026
79ef151
Add missing attribute docstrings
AdrianSosic Apr 7, 2026
59cb4f5
Move search space validation tests to tests/validation/
AdrianSosic Apr 7, 2026
a06753a
Add missing SubspaceContinuous validation tests
AdrianSosic Apr 7, 2026
b8741ed
Validate constraint parameter names in SubspaceContinuous
AdrianSosic Apr 7, 2026
d5561bc
Move simplex validation test to tests/validation/
AdrianSosic Apr 7, 2026
d8324a3
Add missing SubspaceDiscrete validation tests
AdrianSosic Apr 7, 2026
37657d7
Parametrize tests and add hybrid case
AdrianSosic Apr 7, 2026
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 @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
for determining the Pareto front
- Interpoint constraints for continuous search spaces

### Changed
- `SubspaceContinuous` now offers a simpler interface for passing constraints,
no longer requiring users to manually group constraints according to their type

### Breaking Changes
- `ContinuousLinearConstraint.to_botorch` now returns a collection of constraint tuples
instead of a single tuple (needed for interpoint constraints)
Expand All @@ -27,6 +31,8 @@ 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
- `SubspaceDiscrete` ignores any `empty_encoding` when provided
- `SubspaceDiscrete` no longer accepts a `comp_rep` argument
- `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
9 changes: 5 additions & 4 deletions baybe/searchspace/_filtered.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ def from_subspace(
cls, subspace: SubspaceDiscrete, mask_keep: npt.NDArray[np.bool_]
) -> Self:
"""Filter an existing subspace."""
return cls(
**asdict(subspace, filter=lambda attr, _: attr.init, recurse=False),
mask_keep=mask_keep,
)
kwargs = asdict(subspace, filter=lambda attr, _: attr.init, recurse=False)
# Remove deprecated fields (to be dropped with the deprecation)
del kwargs["_empty_encoding"]
del kwargs["_comp_rep"]
return cls(**kwargs, mask_keep=mask_keep)

@override
def get_candidates(self) -> tuple[pd.DataFrame, pd.DataFrame]:
Expand Down
243 changes: 155 additions & 88 deletions baybe/searchspace/continuous.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

import gc
import math
import warnings
from collections.abc import Collection, Iterator, Sequence
from itertools import chain, product
from typing import TYPE_CHECKING, Any, cast

import cattrs.gen
import numpy as np
import pandas as pd
from attrs import define, evolve, field, fields
from attrs import define, evolve, field
from typing_extensions import override

from baybe.constraints import (
Expand All @@ -22,6 +24,7 @@
validate_cardinality_constraint_parameter_bounds,
validate_cardinality_constraints_are_nonoverlapping,
)
from baybe.exceptions import IncompatibilityError
from baybe.parameters import NumericalContinuousParameter
from baybe.parameters.base import ContinuousParameter
from baybe.parameters.numerical import _FixedNumericalContinuousParameter
Expand Down Expand Up @@ -51,31 +54,55 @@
class SubspaceContinuous(SerialMixin):
"""Class for managing continuous subspaces.

Builds the subspace from parameter definitions, keeps
track of search metadata, and provides access to candidate sets and different
parameter views.
Builds the subspace from parameter definitions and optional constraints,
and provides access to candidate sets and different parameter views.
"""

parameters: tuple[NumericalContinuousParameter, ...] = field(
default=(),
converter=sort_parameters,
validator=lambda _, __, x: validate_parameter_names(x),
)
"""The parameters of the subspace."""
"""The parameters spanning the subspace."""

constraints_lin_eq: tuple[ContinuousLinearConstraint, ...] = field(
converter=to_tuple, factory=tuple
constraints: tuple[ContinuousConstraint, ...] = field(
default=(), converter=to_tuple
)
"""Linear equality constraints."""
"""Optional constraints filtering the subspace."""

constraints_lin_ineq: tuple[ContinuousLinearConstraint, ...] = field(
converter=to_tuple, factory=tuple
)
"""Linear inequality constraints."""
def __init__(
self,
parameters: Sequence[NumericalContinuousParameter] | None = None,
constraints: Sequence[ContinuousConstraint] | None = None,
constraints_lin_eq: Sequence[ContinuousLinearConstraint] | None = None,
constraints_lin_ineq: Sequence[ContinuousLinearConstraint] | None = None,
constraints_nonlin: Sequence[ContinuousNonlinearConstraint] | None = None,
):
parameters = list(parameters) if parameters is not None else []
constraints = list(constraints) if constraints is not None else []

n_constraints = len(constraints)
if constraints_lin_eq is not None:
constraints.extend(constraints_lin_eq)
if constraints_lin_ineq is not None:
constraints.extend(constraints_lin_ineq)
if constraints_nonlin is not None:
constraints.extend(constraints_nonlin)

if len(constraints) != n_constraints:
warnings.warn(
"You are using the deprecated `constraints_lin_eq`, "
"`constraints_lin_ineq` and/or `constraints_nonlin` arguments to "
"specify constraints. For backward compatibility, we have "
"automatically merged their content into the `constraints` attribute. "
"However, please update your code to directly use the `constraints` "
"argument instead since the deprecated arguments will be removed in "
"a future version.",
DeprecationWarning,
stacklevel=2,
)

constraints_nonlin: tuple[ContinuousNonlinearConstraint, ...] = field(
converter=to_tuple, factory=tuple
)
"""Nonlinear constraints."""
self.__attrs_init__(parameters, constraints)

@override
def __str__(self) -> str:
Expand Down Expand Up @@ -107,41 +134,40 @@ def __str__(self) -> str:

return to_string(self.__class__.__name__, *fields)

@property
def constraints_lin_eq(self) -> tuple[ContinuousLinearConstraint, ...]:
"""Linear equality constraints."""
return tuple(
c
for c in self.constraints
if isinstance(c, ContinuousLinearConstraint) and c.is_eq
)

@property
def constraints_lin_ineq(self) -> tuple[ContinuousLinearConstraint, ...]:
"""Linear inequality constraints."""
return tuple(
c
for c in self.constraints
if isinstance(c, ContinuousLinearConstraint) and not c.is_eq
)

@property
def constraints_nonlin(self) -> tuple[ContinuousNonlinearConstraint, ...]:
"""Nonlinear constraints."""
return tuple(
c for c in self.constraints if isinstance(c, ContinuousNonlinearConstraint)
)

@property
def constraints_cardinality(self) -> tuple[ContinuousCardinalityConstraint, ...]:
"""Cardinality constraints."""
return tuple(
c
for c in self.constraints_nonlin
for c in self.constraints
if isinstance(c, ContinuousCardinalityConstraint)
)

@constraints_lin_eq.validator
def _validate_constraints_lin_eq(
self, _, lst: list[ContinuousLinearConstraint]
) -> None:
"""Validate linear equality constraints."""
# TODO Remove once eq and ineq constraints are consolidated into one list
if not all(c.is_eq for c in lst):
raise ValueError(
f"The list '{fields(self.__class__).constraints_lin_eq.name}' of "
f"{self.__class__.__name__} only accepts equality constraints, i.e. "
f"the 'operator' for all list items should be '='."
)

@constraints_lin_ineq.validator
def _validate_constraints_lin_ineq(
self, _, lst: list[ContinuousLinearConstraint]
) -> None:
"""Validate linear inequality constraints."""
# TODO Remove once eq and ineq constraints are consolidated into one list
if any(c.is_eq for c in lst):
raise ValueError(
f"The list '{fields(self.__class__).constraints_lin_ineq.name}' of "
f"{self.__class__.__name__} only accepts inequality constraints, i.e. "
f"the 'operator' for all list items should be '>=' or '<='."
)

@property
def n_inactive_parameter_combinations(self) -> int:
"""The number of possible inactive parameter combinations."""
Expand All @@ -159,16 +185,26 @@ def inactive_parameter_combinations(self) -> Iterator[frozenset[str]]:
):
yield frozenset(chain(*combination))

@constraints_nonlin.validator
def _validate_constraints_nonlin(self, _, __) -> None:
"""Validate nonlinear constraints."""
# Note: The passed constraints are accessed indirectly through the property
@constraints.validator
def _validate_constraints(self, _, __) -> None:
"""Validate constraints."""
parameter_names = {p.name for p in self.parameters}
for constraint in self.constraints:
if invalid := set(constraint.parameters) - parameter_names:
raise IncompatibilityError(
f"A constraint of type '{constraint.__class__.__name__}' "
f"references the following parameters that are not part of "
f"the subspace: {invalid}."
)

validate_cardinality_constraints_are_nonoverlapping(
self.constraints_cardinality
)

for con in self.constraints_cardinality:
validate_cardinality_constraint_parameter_bounds(con, self.parameters)
for constraint in self.constraints_cardinality:
validate_cardinality_constraint_parameter_bounds(
constraint, self.parameters
)

def to_searchspace(self) -> SearchSpace:
"""Turn the subspace into a search space with no discrete part."""
Expand All @@ -191,32 +227,16 @@ def from_parameter(cls, parameter: ContinuousParameter) -> SubspaceContinuous:
Returns:
The created subspace.
"""
return cls.from_product([parameter])
return cls([parameter])

@classmethod
def from_product(
cls,
parameters: Sequence[ContinuousParameter],
constraints: Sequence[ContinuousConstraint] | None = None,
) -> SubspaceContinuous:
"""See :class:`baybe.searchspace.core.SearchSpace`."""
constraints = constraints or []
return SubspaceContinuous(
parameters=[p for p in parameters if p.is_continuous],
constraints_lin_eq=[
c
for c in constraints
if (isinstance(c, ContinuousLinearConstraint) and c.is_eq)
],
constraints_lin_ineq=[
c
for c in constraints
if (isinstance(c, ContinuousLinearConstraint) and not c.is_eq)
],
constraints_nonlin=[
c for c in constraints if isinstance(c, ContinuousNonlinearConstraint)
],
)
"""Alias for `SubspaceContinuous.__init__`."""
return SubspaceContinuous(parameters, constraints)

@classmethod
def from_bounds(cls, bounds: pd.DataFrame) -> SubspaceContinuous:
Expand Down Expand Up @@ -336,28 +356,17 @@ def _drop_parameters(self, parameter_names: Collection[str]) -> SubspaceContinuo
"""
return SubspaceContinuous(
parameters=[p for p in self.parameters if p.name not in parameter_names],
constraints_lin_eq=[
constraints=[
c._drop_parameters(parameter_names)
for c in self.constraints_lin_eq
if set(c.parameters) - set(parameter_names)
],
constraints_lin_ineq=[
c._drop_parameters(parameter_names)
for c in self.constraints_lin_ineq
if set(c.parameters) - set(parameter_names)
for c in self.constraints
if (set(c.parameters) - set(parameter_names))
],
)

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

@property
def has_interpoint_constraints(self) -> bool:
Expand Down Expand Up @@ -424,9 +433,9 @@ def _enforce_cardinality_constraints(
return evolve(
self,
parameters=adjusted_parameters,
constraints_nonlin=[
constraints=[
c
for c in self.constraints_nonlin
for c in self.constraints
if not isinstance(c, ContinuousCardinalityConstraint)
],
)
Expand Down Expand Up @@ -670,8 +679,66 @@ def get_parameters_by_name(
return tuple(p for p in self.parameters if p.name in names)


# Register deserialization hook
converter.register_structure_hook(SubspaceContinuous, select_constructor_hook)

# Collect leftover original slotted classes processed by `attrs.define`
gc.collect()

# Uncomment when removing the deprecation:
# converter.register_structure_hook(SubspaceContinuous, select_constructor_hook)

# >>>>> Deprecation
_hook = cattrs.gen.make_dict_structure_fn(SubspaceContinuous, converter)


def _structure_hook(specs: dict, cls: type) -> SubspaceContinuous:
"""Structure hook that supports both constructor dispatch and legacy fields."""
if "constructor" in specs:
return select_constructor_hook(specs, cls)

specs = specs.copy()
specs.pop("type", None)

# Check if any deprecated constraint fields are present
deprecated_keys = {
"constraints_lin_eq",
"constraints_lin_ineq",
"constraints_nonlin",
}
if deprecated_keys & specs.keys():
from baybe.constraints.base import (
ContinuousConstraint,
ContinuousNonlinearConstraint,
)

kwargs: dict[str, Any] = {}
if "parameters" in specs:
kwargs["parameters"] = [
converter.structure(p, NumericalContinuousParameter)
for p in specs["parameters"]
]
if "constraints" in specs:
kwargs["constraints"] = [
converter.structure(c, ContinuousConstraint)
for c in specs["constraints"]
]
if "constraints_lin_eq" in specs:
kwargs["constraints_lin_eq"] = [
converter.structure(c, ContinuousLinearConstraint)
for c in specs["constraints_lin_eq"]
]
if "constraints_lin_ineq" in specs:
kwargs["constraints_lin_ineq"] = [
converter.structure(c, ContinuousLinearConstraint)
for c in specs["constraints_lin_ineq"]
]
if "constraints_nonlin" in specs:
kwargs["constraints_nonlin"] = [
converter.structure(c, ContinuousNonlinearConstraint)
for c in specs["constraints_nonlin"]
]
return SubspaceContinuous(**kwargs)

return _hook(specs, cls)


converter.register_structure_hook(SubspaceContinuous, _structure_hook)
# <<<<< Deprecation
Loading
Loading