Constraint Refactor 2: Turn DiscreteExcludeConstraint into DiscreteSelectionConstraint , Introduce DiscreteFilteringConstraint ABC - #880
Conversation
There was a problem hiding this comment.
🟡 Not ready to approve
There are a few concrete runtime/contract issues in the deprecation wrapper and structuring hook (plus a misleading Polars docstring) that should be fixed before merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Refactors discrete searchspace row-pruning by introducing a dedicated DiscretePruningConstraint ABC with unified “keep-by-default” semantics and an exclude inversion flag, while migrating the legacy DiscreteExcludeConstraint behavior to the new DiscreteFilteringConstraint (with deprecation support).
Changes:
- Introduce
DiscretePruningConstraintand migrate pruning constraints to implement “matching/kept rows” logic with a sharedexcludeinversion mechanism (pandas + Polars paths). - Replace
DiscreteExcludeConstraintusage withDiscreteFilteringConstraint(..., exclude=True)across code, docs, examples, and tests; add deprecation + legacy deserialization redirect. - Rename ordering constant to
DISCRETE_CONSTRAINTS_PRUNING_ORDERand update constrained-product construction to only apply pruning constraints during searchspace creation.
File summaries
| File | Description |
|---|---|
| tests/test_deprecations.py | Adds deprecation warning test and legacy deserialization test for DiscreteExcludeConstraint. |
| tests/test_campaign.py | Updates campaign tests to use DiscreteFilteringConstraint(..., exclude=True) instead of DiscreteExcludeConstraint. |
| tests/serialization/test_constraint_serialization.py | Switches hypothesis-based serialization coverage from exclude to filtering constraint strategy. |
| tests/hypothesis_strategies/constraints.py | Renames/updates hypothesis strategy to generate DiscreteFilteringConstraint with randomized exclude. |
| tests/constraints/test_constrained_cartesian_product.py | Updates ordering constant and scenarios to use pruning terminology and filtering constraint replacement. |
| tests/constraints/test_batch_constraint.py | Updates batch-constraint tests to use DiscreteFilteringConstraint(..., exclude=True). |
| tests/conftest.py | Updates shared fixtures to use DiscreteFilteringConstraint(..., exclude=True) for previous exclusion semantics. |
| examples/Constraints_Discrete/filtering_constraints.py | Renames and updates example to demonstrate filtering constraint with exclude=True for exclusion behavior. |
| docs/concepts/getting_recommendations.md | Updates docs snippet to use DiscreteFilteringConstraint(..., exclude=True). |
| docs/components/constraints.md | Reworks discrete-constraints docs around pruning constraints + exclude semantics; updates examples and links. |
| CHANGELOG.md | Documents new ABC, new constraint, rename, deprecation, and removals. |
| baybe/searchspace/utils.py | Updates constrained cartesian product construction to apply only DiscretePruningConstraints; updates ordering and Polars partitioning. |
| baybe/searchspace/discrete.py | Updates constraint sorting to use pruning order with a safe fallback for non-pruning constraints. |
| baybe/constraints/discrete.py | Implements DiscreteFilteringConstraint, migrates other discrete constraints to pruning ABC, adds deprecated DiscreteExcludeConstraint wrapper and legacy structure hook. |
| baybe/constraints/base.py | Introduces DiscretePruningConstraint with shared invalid/valid computation and Polars support contract. |
| baybe/constraints/init.py | Re-exports new names/constant and keeps legacy DiscreteExcludeConstraint export. |
| baybe/campaign.py | Tightens candidate toggling path to require pruning constraints (instead of all discrete constraints). |
Review details
- Files reviewed: 17/17 changed files
- Comments generated: 4
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
f058d63 to
111368f
Compare
111368f to
c77c116
Compare
AdrianSosic
left a comment
There was a problem hiding this comment.
Not yet entirely done with the review, but already submitting this part because I want to propose an alternative for the deserialization shim
|
|
||
|
|
||
| # >>>>>>>>>> Deprecation | ||
| # NOTE: This block exists solely to redirect legacy constraint type names during |
There was a problem hiding this comment.
I think you are massively overcomplicating this, tbh 😬 The code pretty much copies parts of the deserialization engine that should instead only be referenced! All that is needed is a very simple compat hook that applies the name swapping before calling the existing machinery.
Let me push a proposal for this, if you don't mind 👍🏼
There was a problem hiding this comment.
I would be much happier with that solution as its more concise
I guess I tried to cover too many cases which lead to this long implementation
here are the cases identified, the new solution is not entirely equivalent to the old one, but can we live with that?

In particular: Are we ok with the new behavior in case 2 and 4
There was a problem hiding this comment.
can you provide me the code for 2 and 4 so that I can exactly replicate and see?
There was a problem hiding this comment.
Case 2 - Wrong Input Type + Concrete Selection Constraint as Target
from baybe.constraints import DiscreteSelectionConstraint
from baybe.serialization import converter
data = {
"type": "DiscreteSumConstraint", # intentionally wrong
"parameters": ["A"],
"conditions": [{"type": "SubSelectionCondition", "selection": ["a"]}],
}
result = converter.structure(data, DiscreteSelectionConstraint)
print(result)
# New: builds DiscreteSelectionConstraint
# Old: `ValueError: The 'type' field 'DiscreteSumConstraint' does not match the target class 'DiscreteSelectionConstraint'.`Case 4 - Legacy Type as Input + Concrete Selection Constraint as Target
from baybe.constraints import DiscreteSelectionConstraint
from baybe.serialization import converter
data = {
"type": "DiscreteExcludeConstraint",
"parameters": ["A"],
"conditions": [{"type": "SubSelectionCondition", "selection": ["a"]}],
}
result = converter.structure(data, DiscreteSelectionConstraint)
print(result.exclude)
# New: False
# Old: Truebtw I'm not saying these are necessarily relevant, If we determine theyre not important I'm happy to jsut keep your much leaner versions, but from the looks of it it seems to me the old outcomes for these cases is preferable
AVHopp
left a comment
There was a problem hiding this comment.
LGTM - only two real issues, but we can discuss those in the threads
DiscreteBatchConstraint._get_invalid was never reached: batch constraints are excluded from search-space filtering, the only caller of get_invalid.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
An XOR result can flip as further operands arrive, so it must not be evaluated on a partial column set. Cover all exclude/combiner combinations.
The redirect was only registered on 'DiscretePruningConstraint', but search spaces deserialize constraints via the 'Constraint' and 'DiscreteConstraint' annotations. Register it on the 'Constraint' base and make concrete targets reject a mismatched 'type' field.
Co-authored-by: AdrianSosic <adrian.sosic@merckgroup.com>
05f0d8f to
c36ccc5
Compare
DiscreteExcludeConstraint into DiscreteFilteringConstraint , Introduce DiscretePruningConstraint ABCDiscreteExcludeConstraint into DiscreteSelectionConstraint , Introduce DiscreteFilteringConstraint ABC
Closes #873
Based on #879
Three major components in this PR:
DiscreteFilteringConstraintABC that owns all row-filtering logic applied at searchspace creation. This cleans also the previously less separatedDiscreteBatchConstraintwhich is not derived from itexcludeflag via the new ABC, which inverts the filtering logicDiscreteExcludeConstraintreworked intoDiscreteSelectionConstraint(inverted semantics, deprecated)Notes:
complementflag (imo less nice)