Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2d63d2b
Fix is_numeric typo in _FixedNumericalContinuousParameter
Scienfitz Mar 13, 2026
a84527f
Generalize subspace naming
Scienfitz Mar 12, 2026
0d2a813
Extract _optimize_over_subspaces and add dispatch
Scienfitz Mar 12, 2026
dc8929e
Add hybrid constraint tests
Scienfitz Mar 13, 2026
1c3aaf7
Filter by constraint type in cardinality utilities
Scienfitz Mar 13, 2026
c8928b8
Add DiscreteBatchConstraint class and validation
Scienfitz Mar 18, 2026
8128968
Add partition machinery to SubspaceDiscrete
Scienfitz Mar 18, 2026
5bb43cd
Add shuffle/replace to SubspaceContinuous
Scienfitz Mar 18, 2026
f78dab3
Add partition aggregation to SearchSpace
Scienfitz Mar 18, 2026
48efdfe
Wire recommenders for DiscreteBatchConstraint
Scienfitz Mar 18, 2026
6039baf
Add tests for DiscreteBatchConstraint
Scienfitz Mar 18, 2026
b79643f
Add DiscreteBatchConstraint to constraints userguide
Scienfitz Mar 18, 2026
3417f91
Adjust constraint property names
Scienfitz Apr 1, 2026
77818e9
Improve docstring language
Scienfitz Apr 1, 2026
4e76ccf
Improve partition sampling
Scienfitz Apr 1, 2026
c7597e8
Split BotorchRecommender into submodules
Scienfitz Apr 1, 2026
273567c
Rename subspace to partition
Scienfitz Apr 1, 2026
7fdfb5c
Update CHANGELOG
Scienfitz Mar 13, 2026
5ab2a6d
Improve docstring
Scienfitz Apr 9, 2026
231f82a
Use consistent mask type hint in discrete recommender
Scienfitz Apr 9, 2026
f27cbca
Mention replacement in docstring
Scienfitz Apr 10, 2026
ce9c5e3
Mention computational expense
Scienfitz Apr 10, 2026
f8c1905
Simplify infinite iterator in inactive_parameter_combinations
AdrianSosic May 5, 2026
32a6edf
Disable pydoclint yield type checking (DOC404) globally
AdrianSosic May 5, 2026
86c87b2
Update docstring
Scienfitz May 7, 2026
d212060
Update signature
Scienfitz May 7, 2026
a835cd4
Update docstring
Scienfitz May 7, 2026
41902c3
Fix formatting
Scienfitz May 7, 2026
d76a6a3
Improve language
Scienfitz May 7, 2026
2f14ea6
Improve language
Scienfitz May 7, 2026
5ebccad
Make formatting consistent
Scienfitz May 7, 2026
bc19835
Turn docstring into comment
Scienfitz May 7, 2026
a980be9
Improve tests
Scienfitz May 7, 2026
615b581
Fix post-rebase issues
Scienfitz May 7, 2026
e6b60fc
Refactor constraint parametrization
AdrianSosic May 8, 2026
7a5d725
Extract select_via_flat_index as shared utility
Scienfitz May 8, 2026
1ee36c2
Rename partition to subset
Scienfitz May 8, 2026
86aaaf9
Fix docstring
Scienfitz May 8, 2026
c1b33ac
Add formatting rule to AGENTS.md
Scienfitz May 8, 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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ Custom `@classproperty` from `baybe.utils.basic` for class-level computed proper
- No private field names (`_attr`) in user-facing messages — use
`fields(type(self)).attr.alias`.
- Method names start with verbs. Comments capitalize first word.
- Always capitalize words that correspond to names of inventors, e.g. `Bayesian`,
`Boolean` or `Gaussian`

## 5. Type Annotations
- **Full coverage**: All signatures including returns. Every field annotated.
Expand Down Expand Up @@ -236,7 +238,7 @@ Three tiers:
- Cache invalidation: `on_setattr` hooks on mutable fields.

## 12. Optional Dependencies
1. Detection (`baybe/_optional/info.py`): `importlib.util.find_spec()` sets boolean
1. Detection (`baybe/_optional/info.py`): `importlib.util.find_spec()` sets Boolean
flags (`CHEM_INSTALLED`, `ONNX_INSTALLED`, etc.) without importing.
2. Guarded imports (`baybe/_optional/<dep>.py`): Import or raise
`OptionalImportError` with pip install instructions.
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Coding convention instructions for agentic developers (`AGENTS.md`, `CLAUDE.md`)
- `has_polars_implementation` property on `DiscreteConstraint`
- `allow_missing` flag on `DiscreteConstraint.get_invalid` and `get_valid`
- `DiscreteBatchConstraint` for ensuring all recommendations in a batch share
the same value for a specified discrete parameter

### Breaking Changes
- `parameter_cartesian_prod_pandas` and `parameter_cartesian_prod_polars` moved
Expand All @@ -26,6 +28,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed
- Broken cache validation for certain `Campaign.recommend` cases
- `ContinuousCardinalityConstraint` now works in hybrid search spaces
- Typo in `_FixedNumericalContinuousParameter` where `is_numeric` was used
instead of `is_numerical`
- `SHAPInsight` breaking with `numpy>=2.4` due to no longer accepted implicit array to
scalar conversion
- Using `np.isclose` for assessing equality of `Interval` bounds instead of hard
Expand All @@ -46,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
can now be conveniently controlled via the new `Settings` mechanism

### Deprecations
- `BotorchRecommender.max_n_subspaces` has been renamed to `max_n_partitions`
- `set_random_seed` and `temporary_seed` utility functions
- The environment variables
`BAYBE_NUMPY_USE_SINGLE_PRECISION`/`BAYBE_TORCH_USE_SINGLE_PRECISION` have been
Expand Down
2 changes: 1 addition & 1 deletion baybe/_optional/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


@contextmanager
def exclude_sys_path(path: str, /): # noqa: DOC402, DOC404
def exclude_sys_path(path: str, /): # noqa: DOC402
"""Temporarily remove a specified path from `sys.path`.

Args:
Expand Down
2 changes: 2 additions & 0 deletions baybe/constraints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from baybe.constraints.discrete import (
DISCRETE_CONSTRAINTS_FILTERING_ORDER,
DiscreteBatchConstraint,
DiscreteCardinalityConstraint,
DiscreteCustomConstraint,
DiscreteDependenciesConstraint,
Expand All @@ -33,6 +34,7 @@
"ContinuousLinearEqualityConstraint",
"ContinuousLinearInequalityConstraint",
# --- Discrete constraints ---#
"DiscreteBatchConstraint",
"DiscreteCardinalityConstraint",
"DiscreteCustomConstraint",
"DiscreteDependenciesConstraint",
Expand Down
9 changes: 8 additions & 1 deletion baybe/constraints/continuous.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,14 @@ def to_botorch(
class ContinuousCardinalityConstraint(
CardinalityConstraint, ContinuousNonlinearConstraint
):
"""Class for continuous cardinality constraints."""
"""Class for continuous cardinality constraints.

Notes:
This constraint can lead to overhead in the computation since optimization
results in individual optimizations over several subsets. If there are
multiple subset-generating constraints active, this can drastically increase
the computational cost due to the combinatorial explosion.
"""

relative_threshold: float = field(
default=1e-3, converter=float, validator=[gt(0.0), lt(1.0)]
Expand Down
69 changes: 69 additions & 0 deletions baybe/constraints/discrete.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from functools import reduce
from typing import TYPE_CHECKING, Any, ClassVar, cast

import numpy as np
import numpy.typing as npt
import pandas as pd
from attrs import define, field
from attrs.validators import in_, min_len
Expand Down Expand Up @@ -424,6 +426,72 @@ def _get_invalid(self, df: pd.DataFrame, /) -> pd.Index:
return df.index[mask_bad]


@define
class DiscreteBatchConstraint(DiscreteConstraint):
"""Constraint ensuring recommendations in a batch share certain parameter values.

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

This constraint is not supported by all recommenders. It is not applied during
Comment thread
AVHopp marked this conversation as resolved.
search space creation (all parameter values remain in the search space).

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

Notes:
This constraint can lead to overhead in the computation since optimization
results in individual optimizations over several subsets. If there are
multiple subset-generating constraints active, this can drastically increase
the computational cost due to the combinatorial explosion.
"""

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

def __attrs_post_init__(self):
"""Validate that exactly one parameter is specified."""
if len(self.parameters) != 1:
raise ValueError(
f"'{self.__class__.__name__}' requires exactly one parameter, "
f"but {len(self.parameters)} were provided: {self.parameters}."
Comment on lines +458 to +463
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we get rid of this restriction? My first naive guess would be that it should not be too hard?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

this is unnecessary
it is also already kind of already implemented in the way that you can just use several of such constraints, imo that is much clearer than allowing several parameters for this constraint
allowing >1 parameters here would open the possibility of confusion

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Really? To me, it's rather the current design that is super unintuitive. We have an object that takes parameters (plural!) and then we are only allowed to pass a single one, which needs to be validated using additional logic. And then we allow the option to have several such constraints, which effectively does what would happen if we simply allowed more parameters in the first place.

Where do you see potential confusion? I can only see one way how to interpret a call like parameters=["A", "B"], namely exactly the way you've implemented with several such constraints. The mean can only go columnwise – there is no way that A and B would possibly interfere since they have their own separate value ranges.

Copy link
Copy Markdown
Collaborator Author

@Scienfitz Scienfitz May 7, 2026

Choose a reason for hiding this comment

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

so there is technical debt here causing this confusion, it is just parameters because all constraints currently share this. This is to be altered/fixed in #517 and not in this PR

But it has now driven me into this weird design that the constraint has parameters but only accepts 1 of them

I would much prefer A:

  • that you have exactly one batch constraint per parameter you want to constrain
  • as many batch constraints (for different parameters) as you want

With the alternative being B:

  • only one allowed batch constraint in the searchspace
  • it takes multiple unique parameters

I prefer A because the filtering logic is entirely orthogonal, so expressing it as independent constraints make sense to me. I already made the plan to change this also for the dependencies constraint here #670 so far I have not received any differing opinion

I could live with both variants, but it should be amde consistent with the dependencies as well, so let me know. But please do not make your call based on the parameters name arg

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Or alternative C:

  • allow an arbitrary number of parameters per constraint
  • allow an arbitrary number of such constraints

I don't know why the above should not be possible, and it would be the most natural option for me since:

  • Pretty much all other constraints behave that way (i.e. there is no restriction on the number of parameters for sum constraint, and we can have several of those)
  • It feels natural that one might want to use one such constraint per physical limitation in the system. For example, you have one piece of equipment where temperature/pressure must be synced across the batch, but for the other equipment you need to sync the pH value. --> Two constraints, one with temp/pressure, the other with pH

While you could of course achieve the same with three or just one constraint, why take away the possibility to structure it for no reason?

But I think a third opinion is a good idea here: @AVHopp, @kalama-ai?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think allowing that (option C) would be worst of both worlds - degeneracy in configurations should be avoided especially if its easy (like in this case)

AND

it would be inconsistent with the cardinality constraints where only one set of parameters is specified instead of potentially multiple sets (the set of parameters over which the constraint holds is equivalent to a single parameter in the batch constraint over which the batch constraint holds)

)

@override
def _get_invalid(self, df: pd.DataFrame, /) -> pd.Index:
# Always returns an empty index because this constraint operates at the
# batch level, not the row level. Individual rows are never invalid; the
# constraint is enforced at recommendation time by subsetting candidates
# into subsets.
return pd.Index([])

def subset_masks(
self, candidates_exp: pd.DataFrame, /
) -> list[npt.NDArray[np.bool_]]:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The list type came a bit surprising to me (I saw that you used an Iterator in the searchspace case, which was more in line with my expectation). Any concerns regarding memory here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

thre is this consuming code somehwere down the line total = prod(len(masks) for masks in per_constraint) where per_constraint = [c.partition_masks(candidates_exp) for c in constraints] so len is needed, additionaly I see no memory issue instantiating the entire list as this is linear in the amount of values per parameter and hence small

"""Return Boolean masks defining the subsets for this constraint.

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

Args:
candidates_exp: The experimental representation of candidate points.

Returns:
A list of Boolean masks, one per unique value of the constrained
parameter.
"""
param = self.parameters[0]
return [
Comment thread
AVHopp marked this conversation as resolved.
(candidates_exp[param] == v).values for v in candidates_exp[param].unique()
]


@define
class DiscreteCardinalityConstraint(CardinalityConstraint, DiscreteConstraint):
"""Class for discrete cardinality constraints."""
Expand Down Expand Up @@ -466,6 +534,7 @@ def _get_invalid(self, df: pd.DataFrame, /) -> pd.Index:
DiscreteCustomConstraint,
DiscretePermutationInvarianceConstraint,
DiscreteDependenciesConstraint,
DiscreteBatchConstraint,
)

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

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

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

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

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

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

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

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

supports_discrete_batch_constraints: ClassVar[bool] = False
Comment thread
AdrianSosic marked this conversation as resolved.
"""Class variable indicating whether the recommender supports discrete
batch constraints."""

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

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

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

Expand Down
Loading
Loading