Enable Coefficients for DiscreteSumConstraint and from_simplex#786
Enable Coefficients for DiscreteSumConstraint and from_simplex#786Scienfitz wants to merge 17 commits into
DiscreteSumConstraint and from_simplex#786Conversation
f8b3f17 to
cce898a
Compare
There was a problem hiding this comment.
Pull request overview
This PR extends BayBE’s discrete constraint and search space construction capabilities by adding weighted-sum support to DiscreteSumConstraint and enabling weighted simplex construction via SubspaceDiscrete.from_simplex(simplex_coefficients=...). It also refactors the hot loop in from_simplex to use NumPy arrays for improved performance and memory usage.
Changes:
- Add
coefficientstoDiscreteSumConstraint(defaulting to all ones) and apply weighting in both pandas and polars evaluation paths. - Add keyword-only
simplex_coefficientstoSubspaceDiscrete.from_simplex(defaulting to all ones) and adjust pruning logic to work with weighted sums. - Expand test coverage for the new weighted behaviors and length-mismatch validation.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
baybe/constraints/discrete.py |
Adds DiscreteSumConstraint.coefficients with validation + weighted evaluation (pandas/polars). |
baybe/searchspace/discrete.py |
Adds simplex_coefficients, makes args keyword-only, and rewrites from_simplex construction loop using NumPy with weighted pruning. |
CHANGELOG.md |
Documents the new weighted features and the keyword-only breaking change for from_simplex. |
tests/validation/test_constraint_validation.py |
Adds validation test for DiscreteSumConstraint coefficients length mismatch. |
tests/hypothesis_strategies/constraints.py |
Updates Hypothesis discrete-constraint strategy generation to optionally include coefficients. |
tests/hypothesis_strategies/alternative_creation/test_searchspace.py |
Adds brute-force parity tests for weighted simplex generation and mismatch validation. |
tests/constraints/test_constraints_polars.py |
Adds parity test to ensure polars vs pandas agree for weighted sum constraints. |
tests/constraints/test_constraints_discrete.py |
Adds behavioral tests for weighted sum constraints in the discrete constraint suite. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
466cf21 to
a232085
Compare
AVHopp
left a comment
There was a problem hiding this comment.
Main question is whether or not we still need the assumption on the non-negativity of parameter values
Follows the ContinuousLinearConstraint pattern: coefficients default to all-ones (preserving existing behavior), are validated for length parity with parameters, and the weighted sum is evaluated via a single numpy matrix-vector product to avoid intermediate DataFrame copies.
Reworks the signature to make all optional arguments keyword-only (via *). Adds simplex_coefficients for a weighted simplex sum constraint. The incremental early-pruning algorithm is generalised to handle negative coefficients correctly by computing per-parameter weighted min/max contributions rather than assuming monotonicity, and by keeping nonzero cardinality tracking separate (raw parameter values, coefficient-sign independent). The weighted row-sum uses a single numpy matrix-vector product to avoid intermediate DataFrame copies.
…plex_coefficients Weighted-sum filtering correctness (default and custom coefficients) added to the existing discrete constraint test file, parametrized across all-ones, scaled, negative, and equality operator cases. Simplex coefficient tests (brute-force equivalence, mixed-sign, boundary_only, and equivalence with from_product+DiscreteSumConstraint) added to the existing from_simplex test file. Validation error tests for length mismatch added to the constraint validation test file.
… sum The previous approach (to_numpy() @ np.asarray(coefficients)) consolidates all referenced columns into a contiguous (N, k) array before computing the dot product. When the constraint parameters are non-adjacent columns in the DataFrame this forces a full (N, k) memory copy regardless. For the typical use case of sum constraints (k < 10 parameters), a column-by-column accumulation avoids this: each data[p].to_numpy() is a zero-copy view of a single contiguous column, the scalar multiply produces one (N,) temporary, and the built-in sum accumulates in-place. No (N, k) consolidation allocation is needed. Also removes the now-unused numpy import.
Replaces the pandas-based inner loop (pd.merge cross-join, pd.DataFrame, df.drop inplace) with raw numpy operations (np.repeat + np.tile + np.column_stack for cross-joins, boolean indexing for pruning). The DataFrame is created once at the end. This avoids per-iteration pandas overhead (index management, BlockManager, merge machinery) and reduces peak memory by eliminating duplicate DataFrame+numpy representations.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
a232085 to
3cc4c94
Compare
3cc4c94 to
df93b36
Compare
| evaluate_df = pd.Series( | ||
| sum( | ||
| df[p].to_numpy() * c for p, c in zip(self.parameters, self.coefficients) | ||
| ), | ||
| index=df.index, | ||
| ) |
There was a problem hiding this comment.
| evaluate_df = pd.Series( | |
| sum( | |
| df[p].to_numpy() * c for p, c in zip(self.parameters, self.coefficients) | |
| ), | |
| index=df.index, | |
| ) | |
| evaluate_df = df[self.parameters] @ self.coefficients |
There was a problem hiding this comment.
There was a problem hiding this comment.
So just to be sure I understand this right: you are saying that accessing all columns simultaneously could give a non-contiguous array and that the @ operation will thus result in copying it internally to perform the matrix product? If that's the case, I'm fine with the current version, but could you point me to some docs or similar so that I can read more about it?
There was a problem hiding this comment.
afaik df[self.parameters] will do a copy that is contiguous for the @ operation if the parameters are not referring to the contiguous stored data. Since we are always having a parameter subset here this will prob always happen -> I tried to avoid this
df[p] will only refer to one parameter and hence never do a copy, so unless there are a huge amount of parameters this will be better imo
There was a problem hiding this comment.
So I understand this is only speculative? It could indeed be the case, but just as likely could it be the opposite. Numpy, for example, is where efficient with creating views based on slicing, with no copying involved. And since pandas uses numpy under the hood, there are chances that no copying is involved here, in which case the @-syntax would clearly win in terms of readability AND efficiency (avoiding the loop). So if you decide to deviate from it, then let's turn that speculation into certainty or at least empirical evidence?
There was a problem hiding this comment.
I think you're overestimating what even numpy can do, if you index an array with eg indices 0,3,7 it will always do a copy because that cannot be represented into numpys basepointer/shape/stride model or in other words: it is not a slice. So pandas just inherits from that. Cant spot any speculation here
| np.append(max_nonzero_upcoming, 0), | ||
| ) | ||
| ): | ||
| values = np.asarray(param.values, dtype=float) |
There was a problem hiding this comment.
Since #803, I've mostly rather seen to_numpy calls on the dataframe instead. Is this one here intended/compatible?
There was a problem hiding this comment.
i think this PR is not rebased on that yet and lines like this prob need to be fixed
There was a problem hiding this comment.
after looking at this line I think tis completely unrelated
this lines calls asarray on a tuple (param.values) which always creates a new array, hecn eno problem possible downstream anywhere
There was a problem hiding this comment.
OK, but then I still see one other problem: we have mechanisms that handle the type conversion, such as to_tensor and the dtype variables from Settings, but you are not using any of them. I guess at least the float needs to be replaced with the corresponding settings-attribute, like in other places?
There was a problem hiding this comment.
right, albeit thats completely unrelated to 803 I agree that his shouldnt be haardcoded float
| exp_rep, pd.DataFrame({param.name: param.values}), how="cross" | ||
| n_old = arr.shape[0] | ||
| n_new = len(values) | ||
| arr = np.column_stack( |
There was a problem hiding this comment.
Happy about the numpy-change, but since we're now talking about speed/memory optimizations, let's directly go berserk mode: the current stacking approach is still suboptimal (I think) because it materializes the intermediate arrays. Instead, we should go via broadcasting. I've run a very brief test that promises even further improvements! Can you take care of it?
There was a problem hiding this comment.
i can give it a try but it would help if you could already point out what lines or what kind of lines would be affected in principle by the further improvements
There was a problem hiding this comment.
Yeah sure. So basically starting from the np.column_stack line. Instead of stacking in 2-D, we would not stack explicitly at all but just rearrange the second array along the third dimension. Then, the row sums/cardinalities are computed along that new dimension, where the arrays are implicitly broadcasted against each other, avoiding the materialization of the stacked arrays altogether. Does this already help?
| min_values = [min(p.values) for p in simplex_parameters] | ||
| max_values = [max(p.values) for p in simplex_parameters] | ||
| if not (min(min_values) >= 0.0): | ||
| # Validate non-negativity of raw parameter values (required by the algorithm) |
There was a problem hiding this comment.
The potential issue I asked about in the other thread: my original construction was based on the assumption that parameter contributions are combined only additively! This was used to early-drop invalid combinations since you then know that once surpassed the limits, any additionally included parameter can only make the situation even worse, i.e. push you even further off the limits. Now that you allow possible negative coefficients, the contributions of the individual parameters to the total sum may also be be negative, which breaks the original logic. So what I'm asking: have you checked / how to you ensure that the early-discard-logic is still intact?
There was a problem hiding this comment.
iirc the key element of the previous logic was not that upcoming sum contributions can only increase the sum
the key logic is that the row is doomed if even with the smallest upcoming contribution the sum is already bad (too large etc).
There is nothing in this that requires that an upcoming contribution can be only positive, right? So all this PR did was to correctly identify that minimial upcoming contrib - which now can also be negative due to coeffs
I dont think thats wrong, but in general it will of course lead to much less efficient consturcitons because a negative coeff will lead to less rows that are early dropouts
test_discrete_space_creation_from_simplex_coefficients currecntly tests one case of neg coefficients
There was a problem hiding this comment.
I also think that the logic is correct here, and that this also enables us to allow negative values as well: We only care about contributions, those can be negative or positive. We do not care about the reason for the sign, which could be any combination of positive/negative coefficient with positive/negative value, right? So if we agree that the logic works for negative and positive contributions, then we should be able to drop the assumption on non-negativity for the values as well, right?
There was a problem hiding this comment.
I think so, too 👍🏼 In that case, there is really no reason to forbid any negative parameter values / coefficients
d9f4faa to
2ce4a29
Compare
AVHopp
left a comment
There was a problem hiding this comment.
Only open point from my end is whether or not we want to allow negative values. I am happy with both possible solutions (either investigate and implement now or defer), and since this is captured by a comment, take my approve.
| Raises: | ||
| ValueError: If the passed simplex parameters are not suitable for a simplex | ||
| construction. | ||
| ValueError: If the length of ``simplex_coefficients`` does not match the |
There was a problem hiding this comment.
Should also have an entry for the case of any of the coefficients being 0.
|
|
||
| ## [Unreleased] | ||
| ### Added | ||
| - `coefficients` attribute for `DiscreteSumConstraint`, enabling weighted sums. Follows |
There was a problem hiding this comment.
Needs rebase and corresponding changes in CHANGELOG after release

use-case motivated addition:
DiscreteSumConstraintgets acoefficientsthat works akin to whats been done in the continuous constraintfrom_simplexgets asimplex_coefficientskeyword that allows specifying coefficients for the simplex parameters. This is possible by changing the way the max/min incoming sums are assessed@forfrom_simplexbecause we ensure by construciton that the incoming array is contiguous and does not have to be copied for a reshape and multiplication. This contiguousness is not guaranteed fordata[params]inget_invalidinDiscreteSumConstraintso its more memory efficient to use per-column approaches in the assumption of a small countable amount of parameters (usually the case)Unrelated optimization
I also replaced the inner loop of
from_simplexto use numpy and not pandas. This is also motivated by memory and time efficiency. This commit can be dropped tho if undesired, but there are singificant time and mem savings:(p = simplex parameters, v = values)