Skip to content

Refactor Subspace Constraints#764

Closed
Scienfitz wants to merge 13 commits intomainfrom
refactor/subspace_constraints
Closed

Refactor Subspace Constraints#764
Scienfitz wants to merge 13 commits intomainfrom
refactor/subspace_constraints

Conversation

@Scienfitz
Copy link
Copy Markdown
Collaborator

@Scienfitz Scienfitz commented Mar 13, 2026

Fixes #567

This PR essentially refactors the internal objects and pathways that are invoked for constraints that are implemented via optimizing subspaces. Currently this is only the ContinuousCardinalityConstraint but the planned DiscreteBatchConstraint works in an almost indentical manner which warrants a more unified structure. DiscreteBatchConstraint will come in a subsequent PR (yet to be linked)

This touches mostly internal concepts and no major userfacing things.

As a side effect to the refactoring, the ContinuousCardinalityConstraint can now also be applied in hybrid spaces, which fixes #567

Changes:

  • The properties that were hardcoded in naming to cardinality are renamed to more general names like constraints_cardinality to constraints_subspace_generating
  • The bayesian recommender implements now pathways like
    • discrete / hybrid / continuous will get a with_subspaces and without_subspaces pathway, the former performing the subspace creation and then invoking the latter
    • there is an overall _optimize_subspaces method which is called by all three searchspace type pathways
    • the discrete and hybrid pathways do not yet have all functionality as they will be added in the followup PR, so the full need for this PR might not be easy to see, but when the upcoming discrete batch constraint and a combined hybrid subspace approach are considered it will be clearer
  • _FixedNumericalContinuousParameter had a bugged property which was named is_numeric but it should be is_numerical

Further Suggestions:

  • The file for the botorch recommender has grown rather large and it would benefit form splitting. But since its all in one class that would require outsourcing some code to functions with very large singnatures. If this is acceptable we can split botorch.py into a submodule with core.py, discrete.py, continuous.py, hybrid.py. Personally I think that would improve readability despite long signatures (on private functions)

Rename cardinality-specific names on SubspaceContinuous to general
subspace terminology to enable future subspace-generating constraints:

- constraints_cardinality -> constraints_subspaces
- n_inactive_parameter_combinations -> n_subspaces
- inactive_parameter_combinations() -> subspace_configurations()
- _sample_inactive_parameters() -> _sample_subspace_configurations()
- parameter_names_in_cardinality_constraints -> parameter_names_in_subspace_constraints

Per-constraint methods on ContinuousCardinalityConstraint are unchanged.
Rename cardinality-specific method names on BotorchRecommender:

- _recommend_continuous_with_cardinality_constraints -> _recommend_continuous_with_subspaces
- _recommend_continuous_without_cardinality_constraints -> _recommend_continuous_without_subspaces

Update docstrings and error messages accordingly.
Replace the cardinality-specific _optimize_continuous_subspaces() with a
generic _optimize_over_subspaces() that accepts zero-argument callables
returning (result, acqf_value) tuples. Each callable captures its own
subspace-specific optimization logic via closures.

Establish the _with_subspaces / _without_subspaces dispatch pattern on
all three recommendation paths (discrete, continuous, hybrid):

- _recommend_discrete dispatches to _recommend_discrete_without_subspaces
- _recommend_continuous_with_subspaces uses closures + _optimize_over_subspaces
- _recommend_hybrid dispatches based on continuous.constraints_subspaces
- Add _recommend_hybrid_with_subspaces() which fixes #567:
  ContinuousCardinalityConstraint is now respected in hybrid search spaces
The attribute was named 'is_numeric' instead of 'is_numerical', not
matching the base class attribute declared in Parameter. This caused an
AttributeError when constructing a SearchSpace containing fixed
parameters, e.g. when evolving a SearchSpace with a continuous subspace
modified by _enforce_cardinality_constraints(). The bug was latent
because no existing code path constructed a SearchSpace with
_FixedNumericalContinuousParameter objects until the addition of
_recommend_hybrid_with_subspaces().
Update the docstring to describe the attribute in terms of general
subspace-generating constraints rather than cardinality-specific
terminology.
Add dedicated test file for cardinality constraints in hybrid search
spaces. The test is parametrized with three configurations:

- continuous_only: hybrid space with ContinuousCardinalityConstraint
- discrete_only: hybrid space with DiscreteCardinalityConstraint
- both: hybrid space with both constraint types

Regression test for #567.
Replace ContinuousCardinalityConstraint-specific error messages in
_recommend_continuous_with_subspaces and _recommend_continuous_without_subspaces
with general subspace terminology.

Fix indentation of _recommend_hybrid_with_subspaces which was incorrectly
nested inside _recommend_hybrid_without_subspaces (unreachable after its
return statement).

Remove now-unused ContinuousCardinalityConstraint import.
Use a more descriptive name that clearly communicates the role of
these constraints: they generate subspaces for separate optimization.
This naming convention will also be used for the corresponding
property on SubspaceDiscrete in the follow-up PR.
The test accesses constraints_subspace_generating and then reads
max_cardinality, which is specific to ContinuousCardinalityConstraint.
Filter to only ContinuousCardinalityConstraint instances so the test
remains correct if other subspace-generating constraint types are
added to the property in the future.
The function accesses cardinality-specific attributes (min_cardinality,
max_cardinality, get_absolute_thresholds) on constraints obtained from
constraints_subspace_generating. Since this property may contain
non-cardinality constraints in the future, filter to only
ContinuousCardinalityConstraint instances.
@Scienfitz Scienfitz self-assigned this Mar 13, 2026
@Scienfitz Scienfitz changed the title Refactor/subspace constraints Refactor Subspace Constraints Mar 14, 2026
@Scienfitz Scienfitz marked this pull request as ready for review March 16, 2026 11:06
Copilot AI review requested due to automatic review settings March 16, 2026 11:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors the internal “subspace optimization” pathway used by continuous cardinality constraints so the same machinery can be reused for upcoming subspace-style constraints, and fixes ContinuousCardinalityConstraint enforcement in hybrid search spaces (Issue #567).

Changes:

  • Introduces generalized “subspace-generating constraints” APIs on SubspaceContinuous (renaming former cardinality-specific helpers).
  • Refactors BotorchRecommender to route discrete/continuous/hybrid recommendation through with-/without-subspaces pathways and centralizes subspace selection logic.
  • Fixes _FixedNumericalContinuousParameter class variable name (is_numericis_numerical) and adds/updates tests + changelog.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/constraints/test_cardinality_constraint_hybrid.py Adds coverage to ensure cardinality constraints are respected in hybrid spaces.
tests/constraints/test_cardinality_constraint_continuous.py Updates tests to use the renamed subspace-generating constraint APIs.
baybe/searchspace/continuous.py Renames and generalizes cardinality-subspace helpers (enumeration, counting, sampling).
baybe/recommenders/pure/bayesian/botorch.py Adds explicit with-/without-subspaces recommendation flow and shared subspace optimization routine; enables hybrid cardinality handling.
baybe/parameters/numerical.py Fixes _FixedNumericalContinuousParameter to correctly expose is_numerical.
baybe/constraints/utils.py Updates cardinality validation to use the renamed subspace-generating constraint property.
CHANGELOG.md Documents the hybrid cardinality fix and the _FixedNumericalContinuousParameter typo fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Scienfitz Scienfitz force-pushed the refactor/subspace_constraints branch from 615bff3 to 601ab8a Compare March 16, 2026 11:47
Copy link
Copy Markdown
Collaborator

@AdrianSosic AdrianSosic left a comment

Choose a reason for hiding this comment

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

Hi @Scienfitz. I have not yet fully reviewed the PR but only the changes in searchspace. Wanted to give you an intermediate review since I see a general consistency issue with the changes you introduced here, i.e. you have generalized the concepts from cardinality to subspace-generating but without implementing the necessary adjustments to make things work again in the respective narrower cardinality-based contexts (see comments for details). Let's solve these things first, before we continue the review process 👍🏼

"""The number of possible subspace configurations."""
return math.prod(
c.n_inactive_parameter_combinations for c in self.constraints_cardinality
c.n_inactive_parameter_combinations
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.

I think we have a type issue here. You are accessing c.n_inactive_parameter_combinations here but the only reason why this currently does not throw a mypy error is because you still have the cardinality constraints as the single hard-coded return type of constraints_subspace_generating. As far as I can follow, you are planning to extend this to more constraints, meaning that the method is going to return some form of union type in the future, which will break the logic. I think you already need to take care of this generalization now, because the n_inactive_parameter_combinations call makes semantically no more sense in the generalized subspace naming.

Copy link
Copy Markdown
Collaborator Author

@Scienfitz Scienfitz Mar 30, 2026

Choose a reason for hiding this comment

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

@AdrianSosic
so i now thought about this, what exactly would you have me do here? add a lot of isinstance checks?

We have no other continuous subspace generating constraint yet. So I cant provide a more general interface because I have no idea what it would even look like. Its a bit different for discrete cosntraints, where I can sort of see that the current mask-return is flexible enough to enable all kinds of thinkable discrete subspace generating constraints - but not sure about continuous ones and its sets of names here.

  1. So one option is definitely to revert the renamings and not provide the cosntraints_subspace_generating property and instead have the batch / cardinality flavor in the names. Imo not very pretty but strictly speaking not wrong

  2. What is actually wrong with the current version? You point out it only works due to the type hint of constraints_subspace_generating still being cardinality, but that is factually correct and no issue arises. Non of your comments actually describe broken code, they describe potentially-future-broken code. It will break if I add a new type of constraint - but that is also the point where I have more concrete insight about a potentially more unified return type for cardinality constraints - not now.

  3. One compromise could perhaps be to keep the current renamings but to add the old property for cardinality-only again, but imo thats also weird

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.

Hey @Scienfitz, so here is my take:

  • I understand that there is no other subspace generating constraint yet and that it's thus not clear what the interface should look like (BTW: haven't looked at the follow-up PR yet, but wasn't the point of the renaming that these are the preparations for another subspace-generating constraint to be added in that other PR!?). But your not now is exactly my point: if this part is unclear, then why add incomplete changes toward that direction that introduce semantic inconistencies? It's like a conversation Person A: we need to make this thing more general, to enable some future features then Person B: in what sense, and which features then Person A: I dunno
  • But don't get me wrong: if you have a somewhat clear picture of what that future subspace-generating constraint is going to be, then this is a different story, and we may apply the name changing. But we haven't even talked about it. So: what is it? 🙃
  • Same is true if we want to achieve a consistent naming between discrete and continuous subspaces.
  • But my big problem is that the current version of the code is just inconsistent. The whole point of generalizing from cardinality to subspace-generating is that the latter must be a clear superset of the former – I think we can agree on that, right? But if this is the case, then the points I've marked in the code are simply semantically wrong, and the only reason why mypy does not complain yet is because you haven't yet told it the fact that subspace-generating is superset of cardinality
  • Bottom line: let's discuss the motivation for the name change and maybe it indeed makes sense to apply it, but I think the way it needs to be done is by changing the output type of constraints_subspace_generating to a supertype (e.g. you could simply set it to ContinuousConstraint for now) and then you'll see mypy go crazy, indicating the places that require adaptation. And yes, potentially adding a constraints_cardinality property on top may be the simplest solution to it.

This was just FYI, but maybe we better discuss this in a call?

*[
con.inactive_parameter_combinations()
for con in self.constraints_cardinality
for con in self.constraints_subspace_generating
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.

same issue here

# Note: The passed constraints are accessed indirectly through the property
validate_cardinality_constraints_are_nonoverlapping(
self.constraints_cardinality
self.constraints_subspace_generating
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 pattern continues: this change also currently only work due to the hard-coded cardinality constraint return type of constraints_subspace_generating

)

for con in self.constraints_cardinality:
for con in self.constraints_subspace_generating:
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.

same here

@@ -386,7 +391,7 @@ def _enforce_cardinality_constraints(
"""
# Extract active parameters involved in cardinality constraints
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.

and here as well: the method is for cardinality constraints only, but with your changes it receives names from all subspace-generating constraints.

if len(self.constraints_subspace_generating) == 0:
return self._sample_from_polytope(batch_size, self.comp_rep_bounds.values)

return self._sample_from_polytope_with_cardinality_constraints(batch_size)
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.

what to do for subspace-generating constraints other than cardinality ones? I think there is at least some NotImplementedError needed or similar

) -> pd.DataFrame:
"""Draw random samples from a polytope with cardinality constraints."""
if not self.constraints_cardinality:
if not self.constraints_subspace_generating:
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.

again mismatch between cardinality vs subspace-generating for the given context

while len(samples) < batch_size:
# Randomly set some parameters inactive
inactive_params_sample = self._sample_inactive_parameters(1)[0]
inactive_params_sample = self._sample_subspace_configurations(1)[0]
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.

same here

"""Sample inactive parameters according to the given cardinality constraints."""
def _sample_subspace_configurations(
self, batch_size: int = 1
) -> list[frozenset[str]]:
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.

why the change to frozenset?

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.

to make the sampling and the full enumeration path have consistent return types
why were they not identical before, that def feels strange?

) -> list[frozenset[str]]:
"""Sample subspace configurations according to the given constraints."""
inactives_per_constraint = [
con.sample_inactive_parameters(batch_size)
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.

again mismatch between cardinality vs subspace-generating for the given context

@Scienfitz
Copy link
Copy Markdown
Collaborator Author

abandoning this PR in order to have the entire diff in #765

@Scienfitz Scienfitz closed this Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ContinuousCardinalityConstraint not considered in hybrid spaces

3 participants