From 030a78ff3f3294d74cd13bdcd80cd612c0275ec7 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 09:10:45 +0200 Subject: [PATCH 01/24] Drop SubspaceDiscrete.empty_encoding attribute --- CHANGELOG.md | 1 + baybe/searchspace/core.py | 7 ------- baybe/searchspace/discrete.py | 17 ++--------------- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9931d0d0e..89ab84d4dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - `parallel_runs` argument from `simulate_scenarios`, since parallelization can now be conveniently controlled via the new `Settings` mechanism +- `empty_encoding` attribute from `SubspaceDiscrete` ### Deprecations - `set_random_seed` and `temporary_seed` utility functions diff --git a/baybe/searchspace/core.py b/baybe/searchspace/core.py index 8b0da30c92..36bde21245 100644 --- a/baybe/searchspace/core.py +++ b/baybe/searchspace/core.py @@ -100,7 +100,6 @@ def from_product( cls, parameters: Sequence[Parameter], constraints: Sequence[Constraint] | None = None, - empty_encoding: bool = False, ) -> SearchSpace: """Create a search space from a cartesian product. @@ -114,11 +113,6 @@ def from_product( parameters: The parameters spanning the search space. constraints: An optional set of constraints restricting the valid parameter space. - empty_encoding: If ``True``, uses an "empty" encoding for all parameters. - This is useful, for instance, in combination with random search - strategies that do not read the actual parameter values, since it avoids - the (potentially costly) transformation of the parameter values to their - computational representation. Returns: The constructed search space. @@ -136,7 +130,6 @@ def from_product( discrete = SubspaceDiscrete.from_product( parameters=[p for p in parameters if p.is_discrete], # type:ignore[misc] constraints=[c for c in constraints if c.is_discrete], # type:ignore[misc] - empty_encoding=empty_encoding, ) continuous = SubspaceContinuous.from_product( parameters=[p for p in parameters if p.is_continuous], # type:ignore[misc] diff --git a/baybe/searchspace/discrete.py b/baybe/searchspace/discrete.py index efae2cfc6b..5e50a9624a 100644 --- a/baybe/searchspace/discrete.py +++ b/baybe/searchspace/discrete.py @@ -94,9 +94,6 @@ class SubspaceDiscrete(SerialMixin): exp_rep: pd.DataFrame = field(eq=eq_dataframe) """The experimental representation of the subspace.""" - empty_encoding: bool = field(default=False) - """Flag encoding whether an empty encoding is used.""" - constraints: tuple[DiscreteConstraint, ...] = field( converter=to_tuple, factory=tuple ) @@ -178,7 +175,6 @@ def from_product( cls, parameters: Sequence[DiscreteParameter], constraints: Sequence[DiscreteConstraint] | None = None, - empty_encoding: bool = False, ) -> SubspaceDiscrete: """See :class:`baybe.searchspace.core.SearchSpace`.""" # Set defaults and order constraints @@ -203,10 +199,7 @@ def from_product( _apply_constraint_filter_pandas(df, list(compress(constraints, mask_missing))) return SubspaceDiscrete( - parameters=parameters, - constraints=constraints, - exp_rep=df, - empty_encoding=empty_encoding, + parameters=parameters, constraints=constraints, exp_rep=df ) @classmethod @@ -214,7 +207,6 @@ def from_dataframe( cls, df: pd.DataFrame, parameters: Sequence[DiscreteParameter] | None = None, - empty_encoding: bool = False, ) -> SubspaceDiscrete: """Create a discrete subspace with a specified set of configurations. @@ -231,7 +223,6 @@ def from_dataframe( fallback. For both types, default values are used for their optional arguments. For more details, see :func:`baybe.parameters.utils.get_parameters_from_dataframe`. - empty_encoding: See :func:`baybe.searchspace.core.SearchSpace.from_product`. Returns: The created discrete subspace. @@ -267,7 +258,7 @@ def discrete_parameter_factory( # Ensure dtype consistency df = normalize_input_dtypes(df, parameters) - return cls(parameters=parameters, exp_rep=df, empty_encoding=empty_encoding) + return cls(parameters=parameters, exp_rep=df) @classmethod def from_simplex( @@ -601,10 +592,6 @@ def transform( df, self.parameters, allow_missing=allow_missing, allow_extra=allow_extra ) - # If the transformed values are not required, return an empty dataframe - if self.empty_encoding or len(df) < 1: - return pd.DataFrame(index=df.index) - # Transform the parameters dfs = [] for param in parameters: From 2f41bec2cd09b542a3e6c9538a2cbaf9dff6cca9 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 09:17:50 +0200 Subject: [PATCH 02/24] Drop comp_rep parameter from SubspaceDiscrete.__init__ --- CHANGELOG.md | 1 + baybe/searchspace/discrete.py | 17 ++++++----------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89ab84d4dc..c6fdb4b3da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `parallel_runs` argument from `simulate_scenarios`, since parallelization can now be conveniently controlled via the new `Settings` mechanism - `empty_encoding` attribute from `SubspaceDiscrete` +- `comp_rep` parameter from `SubspaceDiscrete.__init__` ### Deprecations - `set_random_seed` and `temporary_seed` utility functions diff --git a/baybe/searchspace/discrete.py b/baybe/searchspace/discrete.py index 5e50a9624a..e7868fc3fb 100644 --- a/baybe/searchspace/discrete.py +++ b/baybe/searchspace/discrete.py @@ -4,6 +4,7 @@ import gc from collections.abc import Collection, Sequence +from functools import cached_property from itertools import compress from math import prod from typing import TYPE_CHECKING, Any @@ -99,12 +100,6 @@ class SubspaceDiscrete(SerialMixin): ) """A list of constraints for restricting the space.""" - comp_rep: pd.DataFrame = field(eq=eq_dataframe) - """The computational representation of the space. Technically not required but added - as an optional initializer argument to allow ingestion from e.g. serialized objects - and thereby speed up construction. If not provided, the default hook will derive it - from ``exp_rep``.""" - @override def __str__(self) -> str: if self.is_empty: @@ -142,11 +137,6 @@ def _validate_exp_rep( # noqa: DOC101, DOC103 "This is not allowed, as it can lead to hard-to-detect bugs." ) - @comp_rep.default - def _default_comp_rep(self) -> pd.DataFrame: - """Create the default computational representation.""" - return self.transform(self.exp_rep) - def to_searchspace(self) -> SearchSpace: """Turn the subspace into a search space with no continuous part.""" from baybe.searchspace.core import SearchSpace @@ -502,6 +492,11 @@ def parameter_names(self) -> tuple[str, ...]: """Return tuple of parameter names.""" return tuple(p.name for p in self.parameters) + @cached_property + def comp_rep(self) -> pd.DataFrame: + """The computational representation of the subspace.""" + return self.transform(self.exp_rep) + @property def comp_rep_columns(self) -> tuple[str, ...]: """The columns spanning the computational representation.""" From 985a519b632d69c496ba85c10eb68b6a8bcce8aa Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 09:24:58 +0200 Subject: [PATCH 03/24] Fix SubspaceDiscrete class and attribute docstrings --- baybe/searchspace/discrete.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/baybe/searchspace/discrete.py b/baybe/searchspace/discrete.py index e7868fc3fb..2ae539fa56 100644 --- a/baybe/searchspace/discrete.py +++ b/baybe/searchspace/discrete.py @@ -81,16 +81,15 @@ def comp_rep_human_readable(self) -> tuple[float, str]: class SubspaceDiscrete(SerialMixin): """Class for managing discrete subspaces. - Builds the subspace from parameter definitions and optional constraints, 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[DiscreteParameter, ...] = field( converter=sort_parameters, validator=lambda _, __, x: validate_parameter_names(x), ) - """The list of parameters of the subspace.""" + """The parameters spanning the subspace.""" exp_rep: pd.DataFrame = field(eq=eq_dataframe) """The experimental representation of the subspace.""" @@ -98,7 +97,7 @@ class SubspaceDiscrete(SerialMixin): constraints: tuple[DiscreteConstraint, ...] = field( converter=to_tuple, factory=tuple ) - """A list of constraints for restricting the space.""" + """Optional constraints filtering the subspace.""" @override def __str__(self) -> str: From 8cd624f3b88ef0a37a431c034f2ac8624c078f44 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 09:30:06 +0200 Subject: [PATCH 04/24] Add missing validator to SubspaceDiscrete.parameters --- baybe/searchspace/discrete.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/baybe/searchspace/discrete.py b/baybe/searchspace/discrete.py index 2ae539fa56..9395cbd65a 100644 --- a/baybe/searchspace/discrete.py +++ b/baybe/searchspace/discrete.py @@ -12,6 +12,7 @@ import numpy as np import pandas as pd from attrs import define, field +from attrs.validators import deep_iterable, instance_of from cattrs import IterableValidationError from typing_extensions import override @@ -87,11 +88,14 @@ class SubspaceDiscrete(SerialMixin): parameters: tuple[DiscreteParameter, ...] = field( converter=sort_parameters, - validator=lambda _, __, x: validate_parameter_names(x), + validator=[ + deep_iterable(member_validator=instance_of(DiscreteParameter)), + lambda _, __, x: validate_parameter_names(x), + ], ) """The parameters spanning the subspace.""" - exp_rep: pd.DataFrame = field(eq=eq_dataframe) + exp_rep: pd.DataFrame = field(validator=instance_of(pd.DataFrame), eq=eq_dataframe) """The experimental representation of the subspace.""" constraints: tuple[DiscreteConstraint, ...] = field( From b99c0b19cccd6c71f2c8ce595eb3bf6214789c11 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 10:04:19 +0200 Subject: [PATCH 05/24] Clean up SubspaceContinuous attributes Expecting a grouped constaint input from the user is unnecessary since we can also take care of the grouping internally. --- baybe/searchspace/continuous.py | 116 ++++++------------ baybe/searchspace/core.py | 4 +- .../test_cardinality_constraint_continuous.py | 4 +- tests/test_searchspace.py | 7 +- 4 files changed, 44 insertions(+), 87 deletions(-) diff --git a/baybe/searchspace/continuous.py b/baybe/searchspace/continuous.py index a3e0fa34f6..3ff89c9e15 100644 --- a/baybe/searchspace/continuous.py +++ b/baybe/searchspace/continuous.py @@ -10,7 +10,7 @@ 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 ( @@ -62,20 +62,10 @@ class SubspaceContinuous(SerialMixin): ) """The parameters of the subspace.""" - constraints_lin_eq: tuple[ContinuousLinearConstraint, ...] = field( + constraints: tuple[ContinuousConstraint, ...] = field( converter=to_tuple, factory=tuple ) - """Linear equality constraints.""" - - constraints_lin_ineq: tuple[ContinuousLinearConstraint, ...] = field( - converter=to_tuple, factory=tuple - ) - """Linear inequality constraints.""" - - constraints_nonlin: tuple[ContinuousNonlinearConstraint, ...] = field( - converter=to_tuple, factory=tuple - ) - """Nonlinear constraints.""" + """Optional constraints filtering the subspace.""" @override def __str__(self) -> str: @@ -107,41 +97,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.""" @@ -159,10 +148,9 @@ 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.""" validate_cardinality_constraints_are_nonoverlapping( self.constraints_cardinality ) @@ -201,22 +189,7 @@ def from_product( ) -> 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) - ], - ) + return SubspaceContinuous(parameters, constraints) @classmethod def from_bounds(cls, bounds: pd.DataFrame) -> SubspaceContinuous: @@ -336,28 +309,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: @@ -424,9 +386,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) ], ) diff --git a/baybe/searchspace/core.py b/baybe/searchspace/core.py index 36bde21245..6b4948f219 100644 --- a/baybe/searchspace/core.py +++ b/baybe/searchspace/core.py @@ -195,9 +195,7 @@ def constraints(self) -> tuple[Constraint, ...]: """Return the constraints of the search space.""" return ( *self.discrete.constraints, - *self.continuous.constraints_lin_eq, - *self.continuous.constraints_lin_ineq, - *self.continuous.constraints_nonlin, + *self.continuous.constraints, ) @property diff --git a/tests/constraints/test_cardinality_constraint_continuous.py b/tests/constraints/test_cardinality_constraint_continuous.py index 2717ceff42..acb5c4c9e2 100644 --- a/tests/constraints/test_cardinality_constraint_continuous.py +++ b/tests/constraints/test_cardinality_constraint_continuous.py @@ -104,9 +104,7 @@ def test_sampling_cardinality_constraint(cardinality_bounds: tuple[int, int]): ), ) - subspace_continous = SubspaceContinuous( - parameters=parameters, constraints_nonlin=constraints - ) + subspace_continous = SubspaceContinuous(parameters, constraints) with warnings.catch_warnings(record=True) as w: samples = subspace_continous.sample_uniform(BATCH_SIZE) diff --git a/tests/test_searchspace.py b/tests/test_searchspace.py index 7077acbcf5..5d8ef85cd8 100644 --- a/tests/test_searchspace.py +++ b/tests/test_searchspace.py @@ -305,7 +305,7 @@ def test_cardinality_constraints_with_overlapping_parameters(): with pytest.raises(ValueError, match="cannot share the same parameters"): SubspaceContinuous( parameters=parameters, - constraints_nonlin=( + constraints=( ContinuousCardinalityConstraint( parameters=["c1", "c2"], max_cardinality=1, @@ -328,7 +328,7 @@ def test_cardinality_constraint_with_invalid_parameter_bounds(): with pytest.raises(ValueError, match="must include zero"): SubspaceContinuous( parameters=parameters, - constraints_nonlin=( + constraints=( ContinuousCardinalityConstraint( parameters=["c1", "c2"], max_cardinality=1, @@ -485,8 +485,7 @@ def test_sample_from_polytope_mixed_constraints_with_interpoint(): subspace = SubspaceContinuous( parameters=parameters, - constraints_lin_ineq=[regular_constraint], - constraints_lin_eq=[interpoint_constraint], + constraints=[regular_constraint, interpoint_constraint], ) assert subspace.has_interpoint_constraints From 68822734284c711c8934deff983676712f5ab60b Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 10:06:30 +0200 Subject: [PATCH 06/24] Fix SubspaceContinuous class and attribute docstrings --- baybe/searchspace/continuous.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/baybe/searchspace/continuous.py b/baybe/searchspace/continuous.py index 3ff89c9e15..d1e0d2965b 100644 --- a/baybe/searchspace/continuous.py +++ b/baybe/searchspace/continuous.py @@ -51,16 +51,15 @@ 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( converter=sort_parameters, validator=lambda _, __, x: validate_parameter_names(x), ) - """The parameters of the subspace.""" + """The parameters spanning the subspace.""" constraints: tuple[ContinuousConstraint, ...] = field( converter=to_tuple, factory=tuple From e1555d33f216ec7d07d935133a7adbc0d18ade4d Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 10:13:02 +0200 Subject: [PATCH 07/24] Use SubspaceContinuous.__init__ instead of from_product They are now equivalent --- baybe/searchspace/continuous.py | 8 ++++---- baybe/searchspace/core.py | 2 +- .../constraints/test_cardinality_constraint_continuous.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/baybe/searchspace/continuous.py b/baybe/searchspace/continuous.py index d1e0d2965b..af252052d9 100644 --- a/baybe/searchspace/continuous.py +++ b/baybe/searchspace/continuous.py @@ -56,13 +56,14 @@ class SubspaceContinuous(SerialMixin): """ parameters: tuple[NumericalContinuousParameter, ...] = field( + default=(), converter=sort_parameters, validator=lambda _, __, x: validate_parameter_names(x), ) """The parameters spanning the subspace.""" constraints: tuple[ContinuousConstraint, ...] = field( - converter=to_tuple, factory=tuple + default=(), converter=to_tuple ) """Optional constraints filtering the subspace.""" @@ -178,7 +179,7 @@ def from_parameter(cls, parameter: ContinuousParameter) -> SubspaceContinuous: Returns: The created subspace. """ - return cls.from_product([parameter]) + return cls([parameter]) @classmethod def from_product( @@ -186,8 +187,7 @@ def from_product( parameters: Sequence[ContinuousParameter], constraints: Sequence[ContinuousConstraint] | None = None, ) -> SubspaceContinuous: - """See :class:`baybe.searchspace.core.SearchSpace`.""" - constraints = constraints or [] + """Alias for `SubspaceContinuous.__init__`.""" return SubspaceContinuous(parameters, constraints) @classmethod diff --git a/baybe/searchspace/core.py b/baybe/searchspace/core.py index 6b4948f219..1a9c6c0b2d 100644 --- a/baybe/searchspace/core.py +++ b/baybe/searchspace/core.py @@ -131,7 +131,7 @@ def from_product( parameters=[p for p in parameters if p.is_discrete], # type:ignore[misc] constraints=[c for c in constraints if c.is_discrete], # type:ignore[misc] ) - continuous = SubspaceContinuous.from_product( + continuous = SubspaceContinuous( parameters=[p for p in parameters if p.is_continuous], # type:ignore[misc] constraints=[c for c in constraints if c.is_continuous], # type:ignore[misc] ) diff --git a/tests/constraints/test_cardinality_constraint_continuous.py b/tests/constraints/test_cardinality_constraint_continuous.py index acb5c4c9e2..4b8d3f0215 100644 --- a/tests/constraints/test_cardinality_constraint_continuous.py +++ b/tests/constraints/test_cardinality_constraint_continuous.py @@ -153,7 +153,7 @@ def test_polytope_sampling_with_cardinality_constraint(): min_cardinality=MIN_CARDINALITY, ), ] - subspace_continous = SubspaceContinuous.from_product(parameters, constraints) + subspace_continous = SubspaceContinuous(parameters, constraints) with warnings.catch_warnings(record=True) as w: samples = subspace_continous.sample_uniform(BATCH_SIZE) @@ -267,7 +267,7 @@ def test_empty_constraints_after_cardinality_constraint(): min_cardinality=1, ), ] - subspace = SubspaceContinuous.from_product(parameters, constraints) + subspace = SubspaceContinuous(parameters, constraints) subspace.sample_uniform(1) From 42029b65204d46946e94951602b674e0c0cb47cb Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 10:22:09 +0200 Subject: [PATCH 08/24] Add TODO note to SubspaceDiscrete --- baybe/searchspace/discrete.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/baybe/searchspace/discrete.py b/baybe/searchspace/discrete.py index 9395cbd65a..817f793e56 100644 --- a/baybe/searchspace/discrete.py +++ b/baybe/searchspace/discrete.py @@ -95,6 +95,9 @@ class SubspaceDiscrete(SerialMixin): ) """The parameters spanning the subspace.""" + # TODO: When dropping the `exp_rep` parameter from the constructor, + # add a ()-default for `parameters` and declare `from_product` as an alias + # for `__init__` in the docstring (see `SubspaceContinuous` for reference) exp_rep: pd.DataFrame = field(validator=instance_of(pd.DataFrame), eq=eq_dataframe) """The experimental representation of the subspace.""" From 1db9400a59997996789110cfeba1ac6090715582 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 10:59:06 +0200 Subject: [PATCH 09/24] Add deprecation mechanism for legacy constraint arguments --- baybe/searchspace/continuous.py | 45 +++++++++++++++++++++++++++++++++ tests/test_deprecations.py | 35 +++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/baybe/searchspace/continuous.py b/baybe/searchspace/continuous.py index af252052d9..16f4c0cefd 100644 --- a/baybe/searchspace/continuous.py +++ b/baybe/searchspace/continuous.py @@ -4,6 +4,7 @@ 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 @@ -67,6 +68,50 @@ class SubspaceContinuous(SerialMixin): ) """Optional constraints filtering the subspace.""" + _constraints_lin_eq: tuple[ContinuousLinearConstraint, ...] | None = field( + alias="constraints_lin_eq", default=None + ) + """Ignore! For backward compatibility only. Use `constraints` instead.""" + + _constraints_lin_ineq: tuple[ContinuousLinearConstraint, ...] | None = field( + alias="constraints_lin_ineq", default=None + ) + """Ignore! For backward compatibility only. Use `constraints` instead.""" + + _constraints_nonlin: tuple[ContinuousNonlinearConstraint, ...] | None = field( + alias="constraints_nonlin", default=None + ) + """Ignore! For backward compatibility only. Use `constraints` instead.""" + + def __attrs_post_init__(self) -> None: + constraints = list(self.constraints) + n_constraints = len(constraints) + if self._constraints_lin_eq is not None: + constraints.extend(self._constraints_lin_eq) + self._constraints_lin_eq = None + if self._constraints_lin_ineq is not None: + constraints.extend(self._constraints_lin_ineq) + self._constraints_lin_ineq = None + if self._constraints_nonlin is not None: + constraints.extend(self._constraints_nonlin) + self._constraints_nonlin = None + + if len(constraints) == n_constraints: + return + + 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, + ) + self.constraints = constraints + @override def __str__(self) -> str: if self.is_empty: diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py index b1961d31a6..bc99e29cfa 100644 --- a/tests/test_deprecations.py +++ b/tests/test_deprecations.py @@ -20,11 +20,13 @@ ContinuousLinearInequalityConstraint, ) from baybe.constraints.base import Constraint +from baybe.constraints.continuous import ContinuousCardinalityConstraint from baybe.exceptions import DeprecationError from baybe.objectives.desirability import DesirabilityObjective from baybe.objectives.single import SingleTargetObjective from baybe.parameters.enum import SubstanceEncoding from baybe.parameters.numerical import ( + NumericalContinuousParameter, NumericalDiscreteParameter, ) from baybe.recommenders.meta.sequential import TwoPhaseMetaRecommender @@ -32,6 +34,7 @@ BotorchRecommender, ) from baybe.recommenders.pure.nonpredictive.sampling import RandomRecommender +from baybe.searchspace.continuous import SubspaceContinuous from baybe.searchspace.discrete import SubspaceDiscrete from baybe.searchspace.validation import get_transform_parameters from baybe.settings import Settings @@ -575,3 +578,35 @@ def test_deprecated_cache_environment_variables(monkeypatch, value: str, expecte DeprecationWarning, match="'BAYBE_CACHE_DIR' has been deprecated" ): assert Settings(restore_environment=True).cache_directory == expected + + +@pytest.mark.parametrize("positional", [True, False]) +def test_deprecated_constraints_arguments(positional): + """Using the deprecated subspace constraint arguments raises a warning.""" + p = NumericalContinuousParameter("p", (0, 1)) + c = ContinuousLinearConstraint(["p"], "=", [0], 0) + c_lin_eq = ContinuousLinearConstraint(["p"], "=", [1], 0) + c_lin_ineq = ContinuousLinearConstraint(["p"], ">=", [1], 0) + c_nonlin = ContinuousCardinalityConstraint(["p"], 1) + + with pytest.warns(DeprecationWarning): + if positional: + subspace = SubspaceContinuous( + parameters=(p,), + constraints=(c,), + constraints_lin_eq=(c_lin_eq,), + constraints_lin_ineq=(c_lin_ineq,), + constraints_nonlin=(c_nonlin,), + ) + else: + subspace = SubspaceContinuous( + (p,), + (c, c_lin_eq), + (c_lin_ineq,), + (c_nonlin,), + ) + + assert c in subspace.constraints + assert c_lin_eq in subspace.constraints + assert c_lin_ineq in subspace.constraints + assert c_nonlin in subspace.constraints From 2d1be3a0fbea190ea03d94dca88b9b34b8a617c0 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 11:18:54 +0200 Subject: [PATCH 10/24] User custom __init__ instead of deprecation attributes More elegant since it does not require custom unstructuring hook --- baybe/searchspace/continuous.py | 72 ++++++++++++++------------------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/baybe/searchspace/continuous.py b/baybe/searchspace/continuous.py index 16f4c0cefd..afe2f1d327 100644 --- a/baybe/searchspace/continuous.py +++ b/baybe/searchspace/continuous.py @@ -68,49 +68,39 @@ class SubspaceContinuous(SerialMixin): ) """Optional constraints filtering the subspace.""" - _constraints_lin_eq: tuple[ContinuousLinearConstraint, ...] | None = field( - alias="constraints_lin_eq", default=None - ) - """Ignore! For backward compatibility only. Use `constraints` instead.""" - - _constraints_lin_ineq: tuple[ContinuousLinearConstraint, ...] | None = field( - alias="constraints_lin_ineq", default=None - ) - """Ignore! For backward compatibility only. Use `constraints` instead.""" - - _constraints_nonlin: tuple[ContinuousNonlinearConstraint, ...] | None = field( - alias="constraints_nonlin", default=None - ) - """Ignore! For backward compatibility only. Use `constraints` instead.""" + 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 [] - def __attrs_post_init__(self) -> None: - constraints = list(self.constraints) n_constraints = len(constraints) - if self._constraints_lin_eq is not None: - constraints.extend(self._constraints_lin_eq) - self._constraints_lin_eq = None - if self._constraints_lin_ineq is not None: - constraints.extend(self._constraints_lin_ineq) - self._constraints_lin_ineq = None - if self._constraints_nonlin is not None: - constraints.extend(self._constraints_nonlin) - self._constraints_nonlin = None - - if len(constraints) == n_constraints: - return - - 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, - ) - self.constraints = 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, + ) + + self.__attrs_init__(parameters, constraints) @override def __str__(self) -> str: From d4776bc11aac9316586b3469d3b4f5beeca4a9e5 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 11:37:13 +0200 Subject: [PATCH 11/24] Add support for structuring from legacy arguments --- baybe/searchspace/continuous.py | 65 +++++++++++++++++++++++++++++++-- tests/test_deprecations.py | 28 ++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/baybe/searchspace/continuous.py b/baybe/searchspace/continuous.py index afe2f1d327..e053aad60b 100644 --- a/baybe/searchspace/continuous.py +++ b/baybe/searchspace/continuous.py @@ -9,6 +9,7 @@ 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 @@ -666,8 +667,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 diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py index bc99e29cfa..c6cf27cac4 100644 --- a/tests/test_deprecations.py +++ b/tests/test_deprecations.py @@ -610,3 +610,31 @@ def test_deprecated_constraints_arguments(positional): assert c_lin_eq in subspace.constraints assert c_lin_ineq in subspace.constraints assert c_nonlin in subspace.constraints + + +def test_deprecated_constraints_arguments_deserialization(): + """Deserialization from legacy JSON with deprecated constraint attributes works.""" + p1 = NumericalContinuousParameter("p", (0, 1)) + c_lin_eq = ContinuousLinearConstraint(["p"], "=", [1], 1) + c_lin_ineq = ContinuousLinearConstraint(["p"], ">=", [1], 0) + c_nonlin = ContinuousCardinalityConstraint(["p"], 1) + + # Construct the expected object using the modern interface + expected = SubspaceContinuous( + parameters=(p1,), + constraints=(c_lin_eq, c_lin_ineq, c_nonlin), + ) + + # Build a legacy dict with the deprecated constraint field names + legacy_dict = { + "type": "SubspaceContinuous", + "parameters": [p1.to_dict()], + "constraints_lin_eq": [c_lin_eq.to_dict()], + "constraints_lin_ineq": [c_lin_ineq.to_dict()], + "constraints_nonlin": [c_nonlin.to_dict()], + } + + with pytest.warns(DeprecationWarning): + actual = SubspaceContinuous.from_dict(legacy_dict) + + assert actual == expected From 89c92623908bff6405781125a58785032196fd07 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 13:30:45 +0200 Subject: [PATCH 12/24] Add deprecation mechanism for legacy SubspaceDiscrete arguments --- baybe/searchspace/discrete.py | 29 ++++++++++++++++++++++++++--- tests/test_deprecations.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/baybe/searchspace/discrete.py b/baybe/searchspace/discrete.py index 817f793e56..d93bf4f093 100644 --- a/baybe/searchspace/discrete.py +++ b/baybe/searchspace/discrete.py @@ -7,8 +7,9 @@ from functools import cached_property from itertools import compress from math import prod -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Annotated, Any +import cattrs import numpy as np import pandas as pd from attrs import define, field @@ -45,6 +46,14 @@ from baybe.searchspace.core import SearchSpace +def _reject_argument(self, attribute, value): + if value is not None: + raise DeprecationError( + f"Providing '{attribute.alias}' to '{self.__class__.__name__}' is no " + f"longer supported. To proceed, simply drop the argument." + ) + + @define(kw_only=True) class MemorySize: """Estimated memory size of a :class:`SubspaceDiscrete`.""" @@ -101,11 +110,19 @@ class SubspaceDiscrete(SerialMixin): exp_rep: pd.DataFrame = field(validator=instance_of(pd.DataFrame), eq=eq_dataframe) """The experimental representation of the subspace.""" + _empty_encoding: Annotated[bool, cattrs.override(omit=True)] = field( + alias="empty_encoding", default=None, validator=_reject_argument + ) + constraints: tuple[DiscreteConstraint, ...] = field( converter=to_tuple, factory=tuple ) """Optional constraints filtering the subspace.""" + _comp_rep: Annotated[pd.DataFrame, cattrs.override(omit=True)] = field( + alias="comp_rep", default=None, validator=_reject_argument + ) + @override def __str__(self) -> str: if self.is_empty: @@ -171,6 +188,7 @@ def from_product( cls, parameters: Sequence[DiscreteParameter], constraints: Sequence[DiscreteConstraint] | None = None, + empty_encoding: bool | None = None, ) -> SubspaceDiscrete: """See :class:`baybe.searchspace.core.SearchSpace`.""" # Set defaults and order constraints @@ -195,7 +213,10 @@ def from_product( _apply_constraint_filter_pandas(df, list(compress(constraints, mask_missing))) return SubspaceDiscrete( - parameters=parameters, constraints=constraints, exp_rep=df + parameters=parameters, + constraints=constraints, + exp_rep=df, + empty_encoding=empty_encoding, ) @classmethod @@ -203,6 +224,7 @@ def from_dataframe( cls, df: pd.DataFrame, parameters: Sequence[DiscreteParameter] | None = None, + empty_encoding: bool | None = None, ) -> SubspaceDiscrete: """Create a discrete subspace with a specified set of configurations. @@ -219,6 +241,7 @@ def from_dataframe( fallback. For both types, default values are used for their optional arguments. For more details, see :func:`baybe.parameters.utils.get_parameters_from_dataframe`. + empty_encoding: Ignore! For backwards compatibility only. Returns: The created discrete subspace. @@ -254,7 +277,7 @@ def discrete_parameter_factory( # Ensure dtype consistency df = normalize_input_dtypes(df, parameters) - return cls(parameters=parameters, exp_rep=df) + return cls(parameters=parameters, exp_rep=df, empty_encoding=empty_encoding) @classmethod def from_simplex( diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py index c6cf27cac4..d58c8f2917 100644 --- a/tests/test_deprecations.py +++ b/tests/test_deprecations.py @@ -638,3 +638,31 @@ def test_deprecated_constraints_arguments_deserialization(): actual = SubspaceContinuous.from_dict(legacy_dict) assert actual == expected + + +@pytest.mark.parametrize("arg", ["empty_encoding", "comp_rep"]) +def test_deprecated_subspace_discrete_arguments(arg): + """Providing deprecated arguments to `SubspaceDiscrete` raises an error.""" + with pytest.raises(DeprecationError, match=f"Providing '{arg}'"): + SubspaceDiscrete( + parameters=[], constraints=[], exp_rep=pd.DataFrame(), **{arg: 0} + ) + + +def test_deprecated_empty_encoding_from_product(): + """Passing `empty_encoding` to `SubspaceDiscrete.from_product` raises an error.""" + with pytest.raises(DeprecationError, match="Providing 'empty_encoding'"): + SubspaceDiscrete.from_product( + parameters=[NumericalDiscreteParameter("p", [0, 1])], + empty_encoding=True, + ) + + +def test_deprecated_empty_encoding_from_dataframe(): + """Passing `empty_encoding` to `SubspaceDiscrete.from_dataframe` raises an error.""" + with pytest.raises(DeprecationError, match="Providing 'empty_encoding'"): + SubspaceDiscrete.from_dataframe( + parameters=[NumericalDiscreteParameter("p", [0, 1])], + df=pd.DataFrame({"p": [0, 1]}), + empty_encoding=True, + ) From 789dfcedad62fcb9def7f9c2903ef902a1cf7dbd Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 13:39:25 +0200 Subject: [PATCH 13/24] Raise warning instead of error for empty_encoding --- baybe/searchspace/discrete.py | 26 ++++++++++++++++++-------- tests/test_deprecations.py | 17 ++++++++++++----- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/baybe/searchspace/discrete.py b/baybe/searchspace/discrete.py index d93bf4f093..9babf3bfee 100644 --- a/baybe/searchspace/discrete.py +++ b/baybe/searchspace/discrete.py @@ -3,6 +3,7 @@ from __future__ import annotations import gc +import warnings from collections.abc import Collection, Sequence from functools import cached_property from itertools import compress @@ -46,12 +47,21 @@ from baybe.searchspace.core import SearchSpace -def _reject_argument(self, attribute, value): - if value is not None: - raise DeprecationError( - f"Providing '{attribute.alias}' to '{self.__class__.__name__}' is no " - f"longer supported. To proceed, simply drop the argument." - ) +def _deprecate_argument(error: bool): + """Helper for deprecating legacy arguments.""" # noqa: D401 + + def validator(self, attribute, value): + if value is not None: + msg = ( + f"Providing '{attribute.alias}' to '{self.__class__.__name__}' is no " + f"longer supported. To proceed, simply drop the argument." + ) + if error: + raise DeprecationError(msg) + else: + warnings.warn(msg, DeprecationWarning, stacklevel=3) + + return validator @define(kw_only=True) @@ -111,7 +121,7 @@ class SubspaceDiscrete(SerialMixin): """The experimental representation of the subspace.""" _empty_encoding: Annotated[bool, cattrs.override(omit=True)] = field( - alias="empty_encoding", default=None, validator=_reject_argument + alias="empty_encoding", default=None, validator=_deprecate_argument(error=False) ) constraints: tuple[DiscreteConstraint, ...] = field( @@ -120,7 +130,7 @@ class SubspaceDiscrete(SerialMixin): """Optional constraints filtering the subspace.""" _comp_rep: Annotated[pd.DataFrame, cattrs.override(omit=True)] = field( - alias="comp_rep", default=None, validator=_reject_argument + alias="comp_rep", default=None, validator=_deprecate_argument(error=True) ) @override diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py index d58c8f2917..2ed21e24b9 100644 --- a/tests/test_deprecations.py +++ b/tests/test_deprecations.py @@ -640,10 +640,17 @@ def test_deprecated_constraints_arguments_deserialization(): assert actual == expected -@pytest.mark.parametrize("arg", ["empty_encoding", "comp_rep"]) -def test_deprecated_subspace_discrete_arguments(arg): +@pytest.mark.parametrize( + ("arg", "error"), [("empty_encoding", False), ("comp_rep", True)] +) +def test_deprecated_subspace_discrete_arguments(arg, error): """Providing deprecated arguments to `SubspaceDiscrete` raises an error.""" - with pytest.raises(DeprecationError, match=f"Providing '{arg}'"): + context = ( + pytest.raises(DeprecationError, match=f"Providing '{arg}'") + if error + else pytest.warns(DeprecationWarning, match=f"Providing '{arg}'") + ) + with context: SubspaceDiscrete( parameters=[], constraints=[], exp_rep=pd.DataFrame(), **{arg: 0} ) @@ -651,7 +658,7 @@ def test_deprecated_subspace_discrete_arguments(arg): def test_deprecated_empty_encoding_from_product(): """Passing `empty_encoding` to `SubspaceDiscrete.from_product` raises an error.""" - with pytest.raises(DeprecationError, match="Providing 'empty_encoding'"): + with pytest.warns(DeprecationWarning, match="Providing 'empty_encoding'"): SubspaceDiscrete.from_product( parameters=[NumericalDiscreteParameter("p", [0, 1])], empty_encoding=True, @@ -660,7 +667,7 @@ def test_deprecated_empty_encoding_from_product(): def test_deprecated_empty_encoding_from_dataframe(): """Passing `empty_encoding` to `SubspaceDiscrete.from_dataframe` raises an error.""" - with pytest.raises(DeprecationError, match="Providing 'empty_encoding'"): + with pytest.warns(DeprecationWarning, match="Providing 'empty_encoding'"): SubspaceDiscrete.from_dataframe( parameters=[NumericalDiscreteParameter("p", [0, 1])], df=pd.DataFrame({"p": [0, 1]}), From eac2a96821b8b8fe5fc4f08023b409987bb58246 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 13:44:48 +0200 Subject: [PATCH 14/24] Update CHANGELOG.md --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6fdb4b3da..2859922509 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) @@ -25,10 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - `parallel_runs` argument from `simulate_scenarios`, since parallelization can now be conveniently controlled via the new `Settings` mechanism -- `empty_encoding` attribute from `SubspaceDiscrete` -- `comp_rep` parameter from `SubspaceDiscrete.__init__` ### 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 From 39b4f30f14ebf96f78eb8951de2e1f9b6635ea3e Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 15:09:39 +0200 Subject: [PATCH 15/24] Drop dead code The column subselection is no longer needed and would now cause a recursion error due to the involved property lookup --- baybe/searchspace/discrete.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/baybe/searchspace/discrete.py b/baybe/searchspace/discrete.py index 9babf3bfee..ce09defc0f 100644 --- a/baybe/searchspace/discrete.py +++ b/baybe/searchspace/discrete.py @@ -539,9 +539,6 @@ def comp_rep(self) -> pd.DataFrame: @property def comp_rep_columns(self) -> tuple[str, ...]: """The columns spanning the computational representation.""" - # We go via `comp_rep` here instead of using the columns of the individual - # parameters because the search space potentially uses only a subset of the - # columns due to decorrelation return tuple(self.comp_rep.columns) @property @@ -631,15 +628,7 @@ def transform( for param in parameters: comp_df = param.transform(df[param.name]) dfs.append(comp_df) - comp_rep = pd.concat(dfs, axis=1) if dfs else pd.DataFrame() - - # If the computational representation has already been built (with potentially - # removing some columns, e.g. due to decorrelation or dropping constant ones), - # any subsequent transformation should yield the same columns. - try: - return comp_rep[self.comp_rep.columns] - except AttributeError: - return comp_rep + return pd.concat(dfs, axis=1) if dfs else pd.DataFrame() def get_parameters_by_name( self, names: Sequence[str] From a1b23c69c4c280f96fb065379a6aab94348f87a0 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 15:14:16 +0200 Subject: [PATCH 16/24] Add missing test for toggled candidates recommendation --- baybe/searchspace/_filtered.py | 9 +++++---- tests/test_campaign.py | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/baybe/searchspace/_filtered.py b/baybe/searchspace/_filtered.py index 322837c0c5..2663c53d4d 100644 --- a/baybe/searchspace/_filtered.py +++ b/baybe/searchspace/_filtered.py @@ -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]: diff --git a/tests/test_campaign.py b/tests/test_campaign.py index 07776afb63..bce68c438c 100644 --- a/tests/test_campaign.py +++ b/tests/test_campaign.py @@ -117,6 +117,9 @@ def test_candidate_toggling(constraints, exclude, complement): assert all(target == exclude) # must contain the updated values assert all(other != exclude) # must contain the original values + # Assert that recommendation with toggled candidates still works + campaign.recommend(1) + @pytest.mark.parametrize( "flag", From 3b9cc5eebf6957141196bfd75d75a893fc96d68b Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 15:24:16 +0200 Subject: [PATCH 17/24] Add deserialization test for legacy SubspaceDiscrete format --- tests/test_deprecations.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py index 2ed21e24b9..1a7c222b39 100644 --- a/tests/test_deprecations.py +++ b/tests/test_deprecations.py @@ -673,3 +673,17 @@ def test_deprecated_empty_encoding_from_dataframe(): df=pd.DataFrame({"p": [0, 1]}), empty_encoding=True, ) + + +def test_deprecated_discrete_subspace_deserialization(): + """Deserialization from legacy JSON with `empty_encoding`/`comp_rep` works.""" + p = NumericalDiscreteParameter("p", [0, 1]) + expected = SubspaceDiscrete.from_product(parameters=[p]) + + # Build a legacy dict containing the deprecated fields + legacy_dict = expected.to_dict() + legacy_dict["empty_encoding"] = False + legacy_dict["comp_rep"] = legacy_dict["exp_rep"] + + actual = SubspaceDiscrete.from_dict(legacy_dict) + assert actual == expected From 79ef151ab7f0fa1c28eb415710af4f393d2f0f6b Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 15:36:50 +0200 Subject: [PATCH 18/24] Add missing attribute docstrings --- baybe/searchspace/discrete.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/baybe/searchspace/discrete.py b/baybe/searchspace/discrete.py index ce09defc0f..d88b567008 100644 --- a/baybe/searchspace/discrete.py +++ b/baybe/searchspace/discrete.py @@ -123,6 +123,7 @@ class SubspaceDiscrete(SerialMixin): _empty_encoding: Annotated[bool, cattrs.override(omit=True)] = field( alias="empty_encoding", default=None, validator=_deprecate_argument(error=False) ) + "Ignore! For backwards compatibility only." constraints: tuple[DiscreteConstraint, ...] = field( converter=to_tuple, factory=tuple @@ -132,6 +133,7 @@ class SubspaceDiscrete(SerialMixin): _comp_rep: Annotated[pd.DataFrame, cattrs.override(omit=True)] = field( alias="comp_rep", default=None, validator=_deprecate_argument(error=True) ) + "Ignore! For backwards compatibility only." @override def __str__(self) -> str: From 59cb4f513e1803ea095d886062cb5e1cb7885e76 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 15:52:06 +0200 Subject: [PATCH 19/24] Move search space validation tests to tests/validation/ --- tests/test_searchspace.py | 116 ---------------- .../validation/test_searchspace_validation.py | 126 +++++++++++++++++- 2 files changed, 125 insertions(+), 117 deletions(-) diff --git a/tests/test_searchspace.py b/tests/test_searchspace.py index 5d8ef85cd8..c6e4166d60 100644 --- a/tests/test_searchspace.py +++ b/tests/test_searchspace.py @@ -7,10 +7,7 @@ from baybe._optional.info import POLARS_INSTALLED from baybe.constraints import ( - ContinuousCardinalityConstraint, ContinuousLinearConstraint, - DiscreteSumConstraint, - ThresholdCondition, ) from baybe.exceptions import ( EmptySearchSpaceError, @@ -174,77 +171,6 @@ def test_hyperrectangle_searchspace_creation(): assert searchspace.parameters == parameters -def test_invalid_constraint_parameter_combos(): - """Testing invalid constraint-parameter combinations.""" - parameters = [ - CategoricalParameter("cat1", values=("c1", "c2")), - NumericalDiscreteParameter("d1", values=[1, 2, 3]), - NumericalDiscreteParameter("d2", values=[0, 1, 2]), - NumericalContinuousParameter("c1", (0, 2)), - NumericalContinuousParameter("c2", (-1, 1)), - ] - - # Attempting continuous constraint over hybrid parameter set - with pytest.raises(ValueError): - SearchSpace.from_product( - parameters=parameters, - constraints=[ContinuousLinearConstraint(["c1", "c2", "d1"], "=")], - ) - - # Attempting continuous constraint over hybrid parameter set - with pytest.raises(ValueError): - SearchSpace.from_product( - parameters=parameters, - constraints=[ContinuousLinearConstraint(["c1", "c2", "d1"], "=")], - ) - - # Attempting discrete constraint over hybrid parameter set - with pytest.raises(ValueError): - SearchSpace.from_product( - parameters=parameters, - constraints=[ - DiscreteSumConstraint( - parameters=["d1", "d2", "c1"], - condition=ThresholdCondition(threshold=1.0, operator=">"), - ) - ], - ) - - # Attempting constraints over parameter set where a parameter does not exist - with pytest.raises(ValueError): - SearchSpace.from_product( - parameters=parameters, - constraints=[ - DiscreteSumConstraint( - parameters=["d1", "e7", "c1"], - condition=ThresholdCondition(threshold=1.0, operator=">"), - ) - ], - ) - - # Attempting constraints over parameter set where a parameter does not exist - with pytest.raises(ValueError): - SearchSpace.from_product( - parameters=parameters, - constraints=[ContinuousLinearConstraint(["c1", "e7", "d1"], "=")], - ) - - # Attempting constraints over parameter sets containing non-numerical discrete - # parameters. - with pytest.raises( - ValueError, match="valid only for numerical discrete parameters" - ): - SearchSpace.from_product( - parameters=parameters, - constraints=[ - DiscreteSumConstraint( - parameters=["cat1", "d1", "d2"], - condition=ThresholdCondition(threshold=1.0, operator=">"), - ) - ], - ) - - @pytest.mark.parametrize( "parameter_names", [ @@ -295,48 +221,6 @@ def test_searchspace_memory_estimate(searchspace: SearchSpace): ) -def test_cardinality_constraints_with_overlapping_parameters(): - """Creating cardinality constraints with overlapping parameters raises an error.""" - parameters = ( - NumericalContinuousParameter("c1", (0, 1)), - NumericalContinuousParameter("c2", (0, 1)), - NumericalContinuousParameter("c3", (0, 1)), - ) - with pytest.raises(ValueError, match="cannot share the same parameters"): - SubspaceContinuous( - parameters=parameters, - constraints=( - ContinuousCardinalityConstraint( - parameters=["c1", "c2"], - max_cardinality=1, - ), - ContinuousCardinalityConstraint( - parameters=["c2", "c3"], - max_cardinality=1, - ), - ), - ) - - -def test_cardinality_constraint_with_invalid_parameter_bounds(): - """Imposing a cardinality constraint on a parameter whose range does not include - zero raises an error.""" # noqa - parameters = ( - NumericalContinuousParameter("c1", (0, 1)), - NumericalContinuousParameter("c2", (1, 2)), - ) - with pytest.raises(ValueError, match="must include zero"): - SubspaceContinuous( - parameters=parameters, - constraints=( - ContinuousCardinalityConstraint( - parameters=["c1", "c2"], - max_cardinality=1, - ), - ), - ) - - @pytest.mark.skipif( not POLARS_INSTALLED, reason="Optional polars dependency not installed." ) diff --git a/tests/validation/test_searchspace_validation.py b/tests/validation/test_searchspace_validation.py index c18e56c24e..a8e6afe6b6 100644 --- a/tests/validation/test_searchspace_validation.py +++ b/tests/validation/test_searchspace_validation.py @@ -4,7 +4,18 @@ import pytest from pytest import param -from baybe.parameters.numerical import NumericalDiscreteParameter +from baybe.constraints import ( + ContinuousCardinalityConstraint, + ContinuousLinearConstraint, + DiscreteSumConstraint, + ThresholdCondition, +) +from baybe.parameters import ( + CategoricalParameter, + NumericalContinuousParameter, + NumericalDiscreteParameter, +) +from baybe.searchspace import SearchSpace, SubspaceContinuous from baybe.utils.dataframe import get_transform_objects parameters = [NumericalDiscreteParameter("d1", [0, 1])] @@ -42,3 +53,116 @@ def test_invalid_transforms(df, match): def test_valid_transforms(df, missing, extra): """When providing the appropriate flags, the columns of the dataframe to be transformed can be flexibly chosen.""" # noqa get_transform_objects(df, parameters, allow_missing=missing, allow_extra=extra) + + +def test_invalid_constraint_parameter_combos(): + """Testing invalid constraint-parameter combinations.""" + parameters = [ + CategoricalParameter("cat1", values=("c1", "c2")), + NumericalDiscreteParameter("d1", values=[1, 2, 3]), + NumericalDiscreteParameter("d2", values=[0, 1, 2]), + NumericalContinuousParameter("c1", (0, 2)), + NumericalContinuousParameter("c2", (-1, 1)), + ] + + # Attempting continuous constraint over hybrid parameter set + with pytest.raises(ValueError): + SearchSpace.from_product( + parameters=parameters, + constraints=[ContinuousLinearConstraint(["c1", "c2", "d1"], "=")], + ) + + # Attempting continuous constraint over hybrid parameter set + with pytest.raises(ValueError): + SearchSpace.from_product( + parameters=parameters, + constraints=[ContinuousLinearConstraint(["c1", "c2", "d1"], "=")], + ) + + # Attempting discrete constraint over hybrid parameter set + with pytest.raises(ValueError): + SearchSpace.from_product( + parameters=parameters, + constraints=[ + DiscreteSumConstraint( + parameters=["d1", "d2", "c1"], + condition=ThresholdCondition(threshold=1.0, operator=">"), + ) + ], + ) + + # Attempting constraints over parameter set where a parameter does not exist + with pytest.raises(ValueError): + SearchSpace.from_product( + parameters=parameters, + constraints=[ + DiscreteSumConstraint( + parameters=["d1", "e7", "c1"], + condition=ThresholdCondition(threshold=1.0, operator=">"), + ) + ], + ) + + # Attempting constraints over parameter set where a parameter does not exist + with pytest.raises(ValueError): + SearchSpace.from_product( + parameters=parameters, + constraints=[ContinuousLinearConstraint(["c1", "e7", "d1"], "=")], + ) + + # Attempting constraints over parameter sets containing non-numerical discrete + # parameters. + with pytest.raises( + ValueError, match="valid only for numerical discrete parameters" + ): + SearchSpace.from_product( + parameters=parameters, + constraints=[ + DiscreteSumConstraint( + parameters=["cat1", "d1", "d2"], + condition=ThresholdCondition(threshold=1.0, operator=">"), + ) + ], + ) + + +def test_cardinality_constraints_with_overlapping_parameters(): + """Creating cardinality constraints with overlapping parameters raises an error.""" + parameters = ( + NumericalContinuousParameter("c1", (0, 1)), + NumericalContinuousParameter("c2", (0, 1)), + NumericalContinuousParameter("c3", (0, 1)), + ) + with pytest.raises(ValueError, match="cannot share the same parameters"): + SubspaceContinuous( + parameters=parameters, + constraints=( + ContinuousCardinalityConstraint( + parameters=["c1", "c2"], + max_cardinality=1, + ), + ContinuousCardinalityConstraint( + parameters=["c2", "c3"], + max_cardinality=1, + ), + ), + ) + + +def test_cardinality_constraint_with_invalid_parameter_bounds(): + """Imposing a cardinality constraint on a parameter whose range does not include + zero raises an error.""" # noqa + parameters = ( + NumericalContinuousParameter("c1", (0, 1)), + NumericalContinuousParameter("c2", (1, 2)), + ) + with pytest.raises(ValueError, match="must include zero"): + SubspaceContinuous( + parameters=parameters, + constraints=( + ContinuousCardinalityConstraint( + parameters=["c1", "c2"], + max_cardinality=1, + ), + ), + ) From a06753a43318afbe90c582a4bd306cc956dc77be Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 16:03:00 +0200 Subject: [PATCH 20/24] Add missing SubspaceContinuous validation tests Add tests for: - duplicate parameter names (existing validation, missing coverage) - constraints referencing nonexistent parameters (missing validation) --- .../validation/test_searchspace_validation.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/validation/test_searchspace_validation.py b/tests/validation/test_searchspace_validation.py index a8e6afe6b6..205b085791 100644 --- a/tests/validation/test_searchspace_validation.py +++ b/tests/validation/test_searchspace_validation.py @@ -10,6 +10,7 @@ DiscreteSumConstraint, ThresholdCondition, ) +from baybe.exceptions import IncompatibilityError from baybe.parameters import ( CategoricalParameter, NumericalContinuousParameter, @@ -166,3 +167,34 @@ def test_cardinality_constraint_with_invalid_parameter_bounds(): ), ), ) + + +def test_continuous_subspace_with_duplicate_parameter_names(): + """Creating a continuous subspace with duplicate parameter names raises an error.""" + with pytest.raises(ValueError, match="unique names"): + SubspaceContinuous( + parameters=[ + NumericalContinuousParameter("c1", (0, 1)), + NumericalContinuousParameter("c1", (0, 2)), + ] + ) + + +@pytest.mark.parametrize( + "constraint_params", + [ + param(["nonexistent"], id="all_nonexistent"), + param(["c1", "nonexistent"], id="partially_nonexistent"), + ], +) +def test_continuous_subspace_constraint_with_nonexistent_params(constraint_params): + """Using constraints referencing nonexistent parameters raises an error.""" + parameters = [ + NumericalContinuousParameter("c1", (0, 1)), + NumericalContinuousParameter("c2", (0, 1)), + ] + with pytest.raises(IncompatibilityError, match="not part of the subspace"): + SubspaceContinuous( + parameters=parameters, + constraints=[ContinuousLinearConstraint(constraint_params, "=")], + ) From b8741ede17ee40354f9d0881815d73f10313d793 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 16:38:29 +0200 Subject: [PATCH 21/24] Validate constraint parameter names in SubspaceContinuous --- baybe/searchspace/continuous.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/baybe/searchspace/continuous.py b/baybe/searchspace/continuous.py index e053aad60b..947bdc0e8d 100644 --- a/baybe/searchspace/continuous.py +++ b/baybe/searchspace/continuous.py @@ -24,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 @@ -187,12 +188,23 @@ def inactive_parameter_combinations(self) -> Iterator[frozenset[str]]: @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.""" From d5561bcfeb81633db2a3e97da1cf2658c6b7357c Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 16:57:51 +0200 Subject: [PATCH 22/24] Move simplex validation test to tests/validation/ --- tests/test_searchspace.py | 28 ++++--------------- .../validation/test_searchspace_validation.py | 20 ++++++++++++- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/tests/test_searchspace.py b/tests/test_searchspace.py index c6e4166d60..6bfc5303a8 100644 --- a/tests/test_searchspace.py +++ b/tests/test_searchspace.py @@ -118,24 +118,6 @@ def test_discrete_from_dataframe_dtype_consistency(): assert pd.api.types.is_float_dtype(subspace.exp_rep["C"]) -def test_invalid_simplex_creating_with_overlapping_parameters(): - """Creating a simplex searchspace with overlapping simplex and product parameters - raises an error.""" # noqa - parameters = [NumericalDiscreteParameter(name="x_1", values=(0, 1, 2))] - - with pytest.raises( - ValueError, - match="'simplex_parameters' and 'product_parameters' must be disjoint", - ): - SearchSpace( - SubspaceDiscrete.from_simplex( - max_sum=1.0, - simplex_parameters=parameters, - product_parameters=parameters, - ) - ) - - def test_continuous_searchspace_creation_from_bounds(): """A purely continuous search space is created from example bounds.""" parameters = ( @@ -314,14 +296,16 @@ def test_task_parameter_active_values_validation(): [ ( ["InterConstraint_3"], - lambda samples: samples["Conti_finite1"].sum() - + 2 * samples["Conti_finite2"].sum(), + lambda samples: ( + samples["Conti_finite1"].sum() + 2 * samples["Conti_finite2"].sum() + ), lambda result: np.isclose(result, 0.3, atol=1e-6), ), ( ["InterConstraint_4"], - lambda samples: 2 * samples["Conti_finite1"].sum() - - samples["Conti_finite2"].sum(), + lambda samples: ( + 2 * samples["Conti_finite1"].sum() - samples["Conti_finite2"].sum() + ), lambda result: result >= 0.3 - 1e-6, ), ], diff --git a/tests/validation/test_searchspace_validation.py b/tests/validation/test_searchspace_validation.py index 205b085791..37f1f77a54 100644 --- a/tests/validation/test_searchspace_validation.py +++ b/tests/validation/test_searchspace_validation.py @@ -16,7 +16,7 @@ NumericalContinuousParameter, NumericalDiscreteParameter, ) -from baybe.searchspace import SearchSpace, SubspaceContinuous +from baybe.searchspace import SearchSpace, SubspaceContinuous, SubspaceDiscrete from baybe.utils.dataframe import get_transform_objects parameters = [NumericalDiscreteParameter("d1", [0, 1])] @@ -198,3 +198,21 @@ def test_continuous_subspace_constraint_with_nonexistent_params(constraint_param parameters=parameters, constraints=[ContinuousLinearConstraint(constraint_params, "=")], ) + + +def test_invalid_simplex_creation_with_overlapping_parameters(): + """Creating a simplex searchspace with overlapping simplex and product parameters + raises an error.""" # noqa + parameters = [NumericalDiscreteParameter(name="x_1", values=(0, 1, 2))] + + with pytest.raises( + ValueError, + match="'simplex_parameters' and 'product_parameters' must be disjoint", + ): + SearchSpace( + SubspaceDiscrete.from_simplex( + max_sum=1.0, + simplex_parameters=parameters, + product_parameters=parameters, + ) + ) From d8324a3737c53e810707075eba224aae0427f30d Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 21:08:53 +0200 Subject: [PATCH 23/24] Add missing SubspaceDiscrete validation tests Add tests for: - duplicate parameter names (existing validation, missing coverage) - constraints referencing nonexistent parameters (missing validation) --- .../validation/test_searchspace_validation.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/validation/test_searchspace_validation.py b/tests/validation/test_searchspace_validation.py index 37f1f77a54..f6844074e6 100644 --- a/tests/validation/test_searchspace_validation.py +++ b/tests/validation/test_searchspace_validation.py @@ -10,6 +10,7 @@ DiscreteSumConstraint, ThresholdCondition, ) +from baybe.constraints.discrete import DiscreteLinkedParametersConstraint from baybe.exceptions import IncompatibilityError from baybe.parameters import ( CategoricalParameter, @@ -216,3 +217,34 @@ def test_invalid_simplex_creation_with_overlapping_parameters(): product_parameters=parameters, ) ) + + +def test_discrete_subspace_with_duplicate_parameter_names(): + """Creating a discrete subspace with duplicate parameter names raises an error.""" + with pytest.raises(ValueError, match="unique names"): + SubspaceDiscrete.from_product( + parameters=[ + NumericalDiscreteParameter("d1", values=[0, 1]), + NumericalDiscreteParameter("d1", values=[0, 1, 2]), + ], + ) + + +@pytest.mark.parametrize( + "constraint_params", + [ + param(["nonexistent"], id="all_nonexistent"), + param(["d1", "nonexistent"], id="partially_nonexistent"), + ], +) +def test_discrete_subspace_constraint_with_nonexistent_params(constraint_params): + """Using constraints referencing nonexistent parameters raises an error.""" + parameters = [ + NumericalDiscreteParameter("d1", values=[0, 1]), + NumericalDiscreteParameter("d2", values=[0, 1]), + ] + with pytest.raises(IncompatibilityError, match="not part of the subspace"): + SubspaceDiscrete.from_product( + parameters=parameters, + constraints=[DiscreteLinkedParametersConstraint(constraint_params)], + ) From 37657d79c8a10ddbce959b87d68a2a88f73b5a2b Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Tue, 7 Apr 2026 21:48:09 +0200 Subject: [PATCH 24/24] Parametrize tests and add hybrid case --- .../validation/test_searchspace_validation.py | 85 ++++++++----------- 1 file changed, 35 insertions(+), 50 deletions(-) diff --git a/tests/validation/test_searchspace_validation.py b/tests/validation/test_searchspace_validation.py index f6844074e6..bdd154c5a1 100644 --- a/tests/validation/test_searchspace_validation.py +++ b/tests/validation/test_searchspace_validation.py @@ -170,35 +170,51 @@ def test_cardinality_constraint_with_invalid_parameter_bounds(): ) -def test_continuous_subspace_with_duplicate_parameter_names(): - """Creating a continuous subspace with duplicate parameter names raises an error.""" +p_cont = NumericalContinuousParameter("p", (0, 1)) +p_disc = NumericalDiscreteParameter("p", (0, 1)) + + +@pytest.mark.parametrize( + ("p1", "p2", "space"), + [ + param(p_cont, p_cont, SubspaceContinuous, id="continuous"), + param(p_disc, p_disc, SubspaceDiscrete, id="discrete"), + param(p_cont, p_disc, SearchSpace, id="hybrid"), + ], +) +def test_subspace_with_duplicate_parameter_names(p1, p2, space): + """Creating a search space with duplicate parameter names raises an error.""" with pytest.raises(ValueError, match="unique names"): - SubspaceContinuous( - parameters=[ - NumericalContinuousParameter("c1", (0, 1)), - NumericalContinuousParameter("c1", (0, 2)), - ] - ) + space.from_product(parameters=[p1, p2]) +@pytest.mark.parametrize("discrete", [True, False]) @pytest.mark.parametrize( - "constraint_params", + "referenced", [ param(["nonexistent"], id="all_nonexistent"), - param(["c1", "nonexistent"], id="partially_nonexistent"), + param(["p1", "nonexistent"], id="partially_nonexistent"), ], ) -def test_continuous_subspace_constraint_with_nonexistent_params(constraint_params): +def test_continuous_subspace_constraint_with_nonexistent_params(referenced, discrete): """Using constraints referencing nonexistent parameters raises an error.""" - parameters = [ - NumericalContinuousParameter("c1", (0, 1)), - NumericalContinuousParameter("c2", (0, 1)), - ] + if discrete: + parameters = [ + NumericalDiscreteParameter("p1", (0, 1)), + NumericalDiscreteParameter("p2", (0, 1)), + ] + space = SubspaceDiscrete + constraint = DiscreteLinkedParametersConstraint(referenced) + else: + parameters = [ + NumericalContinuousParameter("p1", (0, 1)), + NumericalContinuousParameter("p2", (0, 1)), + ] + space = SubspaceContinuous + constraint = ContinuousLinearConstraint(referenced, "=") + with pytest.raises(IncompatibilityError, match="not part of the subspace"): - SubspaceContinuous( - parameters=parameters, - constraints=[ContinuousLinearConstraint(constraint_params, "=")], - ) + space.from_product(parameters=parameters, constraints=[constraint]) def test_invalid_simplex_creation_with_overlapping_parameters(): @@ -217,34 +233,3 @@ def test_invalid_simplex_creation_with_overlapping_parameters(): product_parameters=parameters, ) ) - - -def test_discrete_subspace_with_duplicate_parameter_names(): - """Creating a discrete subspace with duplicate parameter names raises an error.""" - with pytest.raises(ValueError, match="unique names"): - SubspaceDiscrete.from_product( - parameters=[ - NumericalDiscreteParameter("d1", values=[0, 1]), - NumericalDiscreteParameter("d1", values=[0, 1, 2]), - ], - ) - - -@pytest.mark.parametrize( - "constraint_params", - [ - param(["nonexistent"], id="all_nonexistent"), - param(["d1", "nonexistent"], id="partially_nonexistent"), - ], -) -def test_discrete_subspace_constraint_with_nonexistent_params(constraint_params): - """Using constraints referencing nonexistent parameters raises an error.""" - parameters = [ - NumericalDiscreteParameter("d1", values=[0, 1]), - NumericalDiscreteParameter("d2", values=[0, 1]), - ] - with pytest.raises(IncompatibilityError, match="not part of the subspace"): - SubspaceDiscrete.from_product( - parameters=parameters, - constraints=[DiscreteLinkedParametersConstraint(constraint_params)], - )