Skip to content

Enable Coefficients for DiscreteSumConstraint and from_simplex#786

Open
Scienfitz wants to merge 17 commits into
mainfrom
feature/sum_constraint_coefficients
Open

Enable Coefficients for DiscreteSumConstraint and from_simplex#786
Scienfitz wants to merge 17 commits into
mainfrom
feature/sum_constraint_coefficients

Conversation

@Scienfitz

@Scienfitz Scienfitz commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator

use-case motivated addition:

  • DiscreteSumConstraint gets a coefficients that works akin to whats been done in the continuous constraint
  • from_simplex gets a simplex_coefficients keyword that allows specifying coefficients for the simplex parameters. This is possible by changing the way the max/min incoming sums are assessed
  • I'm using matrix multiplication @ for from_simplex because 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 for data[params] in get_invalid in DiscreteSumConstraint so 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_simplex to 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:

Scenario Rows main time (s) feature time (s) Δ time main mem (MB) feature mem (MB) Δ mem
4p × 11v 1,001 0.013 0.002 -88% 0.4 0.2 -49%
6p × 11v 8,008 0.039 0.002 -94% 4.3 3.2 -26%
8p × 11v 43,758 0.193 0.011 -94% 35.1 27.9 -20%
6p × 21v 230,230 0.672 0.032 -95% 141.1 106.1 -25%
6p × 21v boundary 53,130 0.736 0.032 -96% 141.1 106.1 -25%

(p = simplex parameters, v = values)

@Scienfitz Scienfitz self-assigned this Apr 29, 2026
@Scienfitz Scienfitz added enhancement Expand / change existing functionality new feature New functionality labels Apr 29, 2026
@Scienfitz Scienfitz force-pushed the feature/sum_constraint_coefficients branch from f8b3f17 to cce898a Compare May 7, 2026 11:20
@Scienfitz Scienfitz marked this pull request as ready for review May 7, 2026 11:48
Copilot AI review requested due to automatic review settings May 7, 2026 11:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 coefficients to DiscreteSumConstraint (defaulting to all ones) and apply weighting in both pandas and polars evaluation paths.
  • Add keyword-only simplex_coefficients to SubspaceDiscrete.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.

Comment thread baybe/searchspace/discrete.py
Comment thread baybe/searchspace/discrete.py Outdated
@Scienfitz Scienfitz force-pushed the feature/sum_constraint_coefficients branch from 466cf21 to a232085 Compare May 7, 2026 12:29

@AVHopp AVHopp left a comment

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.

Main question is whether or not we still need the assumption on the non-negativity of parameter values

Comment thread CHANGELOG.md Outdated
Comment thread CHANGELOG.md
Comment thread baybe/searchspace/discrete.py
Comment thread baybe/searchspace/discrete.py
Comment thread baybe/searchspace/discrete.py
Comment thread baybe/searchspace/discrete.py
@Scienfitz Scienfitz added this to the 0.16.0 milestone Jun 2, 2026
Scienfitz and others added 9 commits June 3, 2026 17:57
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>
@Scienfitz Scienfitz force-pushed the feature/sum_constraint_coefficients branch from a232085 to 3cc4c94 Compare June 3, 2026 16:03
@Scienfitz Scienfitz force-pushed the feature/sum_constraint_coefficients branch from 3cc4c94 to df93b36 Compare June 3, 2026 16:56
Comment on lines +136 to +141
evaluate_df = pd.Series(
sum(
df[p].to_numpy() * c for p, c in zip(self.parameters, self.coefficients)
),
index=df.index,
)

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.

Suggested change
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

@Scienfitz Scienfitz Jun 10, 2026

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.

did you see this comment in the PR description
image

i am prioritizing not doing copy operations here at the cost of having to do several computations instead of one big vectorized one. in the limit of few parameters (generally the case for us) this should be the better choice

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.

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?

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.

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

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.

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?

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 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

Comment thread CHANGELOG.md
Comment thread baybe/searchspace/discrete.py
np.append(max_nonzero_upcoming, 0),
)
):
values = np.asarray(param.values, dtype=float)

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.

Since #803, I've mostly rather seen to_numpy calls on the dataframe instead. Is this one here intended/compatible?

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 this PR is not rebased on that yet and lines like this prob need to be fixed

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.

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

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.

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?

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.

right, albeit thats completely unrelated to 803 I agree that his shouldnt be haardcoded float

Comment thread baybe/searchspace/discrete.py
exp_rep, pd.DataFrame({param.name: param.values}), how="cross"
n_old = arr.shape[0]
n_new = len(values)
arr = np.column_stack(

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.

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?

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 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

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.

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?

Comment thread tests/constraints/test_constraints_discrete.py
Comment thread tests/constraints/test_constraints_polars.py
Comment thread tests/validation/test_constraint_validation.py Outdated
Comment thread tests/hypothesis_strategies/alternative_creation/test_searchspace.py Outdated
Comment thread baybe/searchspace/discrete.py
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)

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 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?

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.

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

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 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?

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 so, too 👍🏼 In that case, there is really no reason to forbid any negative parameter values / coefficients

@Scienfitz Scienfitz force-pushed the feature/sum_constraint_coefficients branch from d9f4faa to 2ce4a29 Compare June 10, 2026 17:51

@AVHopp AVHopp left a comment

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.

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

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.

Should also have an entry for the case of any of the coefficients being 0.

Comment thread CHANGELOG.md

## [Unreleased]
### Added
- `coefficients` attribute for `DiscreteSumConstraint`, enabling weighted sums. Follows

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.

Needs rebase and corresponding changes in CHANGELOG after release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Expand / change existing functionality new feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants