Skip to content

Commit d8bb2a8

Browse files
authored
Merge: Enable Coefficients for DiscreteSumConstraint and from_simplex (#786)
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) - The restriction that `values` of simplex parameters must be positive has been lifted (no longer needed) **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.025 | 0.005 | -81% | 0.3 | 0.2 | -48% | | 6p × 11v | 8,008 | 0.068 | 0.005 | -93% | 4.1 | 1.6 | -62% | | 8p × 11v | 43,758 | 0.280 | 0.010 | -96% | 32.9 | 10.8 | -67% | | 6p × 21v | 230,230 | 0.904 | 0.019 | -98% | 137.3 | 46.2 | -66% | | 6p × 21v boundary | 53,130 | 0.961 | 0.020 | -98% | 137.3 | 45.5 | -67% | (p = simplex parameters, v = values)
2 parents e609fc5 + 0884e25 commit d8bb2a8

10 files changed

Lines changed: 433 additions & 150 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Breaking Changes
9+
- All optional arguments of `SubspaceDiscrete.from_simplex` after `simplex_parameters`
10+
are now keyword-only
11+
12+
### Added
13+
- `coefficients` attribute for `DiscreteSumConstraint`, enabling weighted sums. Follows
14+
the same pattern as `ContinuousLinearConstraint.coefficients`
15+
- `simplex_coefficients` keyword argument to `SubspaceDiscrete.from_simplex` for
16+
weighted simplex sum constraints
17+
818
### Changed
919
- `BOTORCH` GP preset now includes `BetaPrior(2.5, 1.5)` for the task covariance
1020
kernel in multi-task scenarios, matching BoTorch's `MultiTaskGP` defaults introduced
1121
in version `0.18.0`
1222
- The `BOTORCH` GP preset now requires BoTorch `>= 0.18.0` and raises an
1323
`IncompatibilityError` if an older version is installed
1424
- Minimum required polars version increased to `0.20.8`
25+
- `DiscreteSumConstraint`, `ContinuousLinearConstraint`, and
26+
`SubspaceDiscrete.from_simplex` now forbid 0 as coefficients
27+
- `SubspaceDiscrete.from_simplex` no longer requires non-negative parameter values
1528

1629
## [0.15.0] - 2026-06-11
1730
### Breaking Changes

baybe/constraints/continuous.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ def _validate_coefficients( # noqa: DOC101, DOC103
8181
"The given 'coefficients' list must have one floating point entry for "
8282
"each entry in 'parameters'."
8383
)
84+
if any(c == 0.0 for c in coefficients):
85+
raise ValueError("All entries in 'coefficients' must be non-zero.")
8486

8587
@coefficients.default
8688
def _default_coefficients(self) -> tuple[float, ...]:

baybe/constraints/discrete.py

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
from __future__ import annotations
44

55
import gc
6-
from collections.abc import Callable
6+
from collections.abc import Callable, Sequence
77
from functools import reduce
88
from typing import TYPE_CHECKING, Any, ClassVar, cast
99

10+
import cattrs
1011
import numpy as np
1112
import numpy.typing as npt
1213
import pandas as pd
1314
from attrs import define, field
14-
from attrs.validators import in_, min_len
15+
from attrs.validators import deep_iterable, in_, min_len
1516
from typing_extensions import override
1617

1718
from baybe.constraints.base import CardinalityConstraint, DiscreteConstraint
@@ -26,6 +27,7 @@
2627
block_serialization_hook,
2728
converter,
2829
)
30+
from baybe.utils.validation import finite_float
2931

3032
if TYPE_CHECKING:
3133
import polars as pl
@@ -77,7 +79,11 @@ def get_invalid_polars(self) -> pl.Expr:
7779

7880
@define
7981
class DiscreteSumConstraint(DiscreteConstraint):
80-
"""Class for modelling sum constraints."""
82+
"""Class for modelling sum constraints.
83+
84+
The constraint evaluates whether the (optionally weighted) sum of the specified
85+
parameters satisfies the given threshold condition.
86+
"""
8187

8288
# IMPROVE: refactor `SumConstraint` and `ProdConstraint` to avoid code copying
8389

@@ -94,9 +100,45 @@ class DiscreteSumConstraint(DiscreteConstraint):
94100
condition: ThresholdCondition = field()
95101
"""The condition modeled by this constraint."""
96102

103+
coefficients: tuple[float, ...] = field(
104+
converter=lambda x: cattrs.structure(x, tuple[float, ...]),
105+
validator=deep_iterable(member_validator=finite_float),
106+
)
107+
"""The coefficients for the weighted sum, one per entry in ``parameters``.
108+
109+
Defaults to all-ones, i.e. an unweighted sum."""
110+
111+
@coefficients.default
112+
def _default_coefficients(self) -> tuple[float, ...]:
113+
"""Return equal weight coefficients as default."""
114+
return (1.0,) * len(self.parameters)
115+
116+
@coefficients.validator
117+
def _validate_coefficients( # noqa: DOC101, DOC103
118+
self, _: Any, coefficients: Sequence[float]
119+
) -> None:
120+
"""Validate the coefficients.
121+
122+
Raises:
123+
ValueError: If the number of coefficients does not match the number of
124+
parameters.
125+
"""
126+
if len(self.parameters) != len(coefficients):
127+
raise ValueError(
128+
"The given 'coefficients' list must have one floating point entry for "
129+
"each entry in 'parameters'."
130+
)
131+
if any(c == 0.0 for c in coefficients):
132+
raise ValueError("All entries in 'coefficients' must be non-zero.")
133+
97134
@override
98135
def _get_invalid(self, df: pd.DataFrame, /) -> pd.Index:
99-
evaluate_df = df[self.parameters].sum(axis=1)
136+
evaluate_df = pd.Series(
137+
sum(
138+
df[p].to_numpy() * c for p, c in zip(self.parameters, self.coefficients)
139+
),
140+
index=df.index,
141+
)
100142
mask_bad = ~self.condition.evaluate(evaluate_df)
101143

102144
return df.index[mask_bad]
@@ -105,7 +147,8 @@ def _get_invalid(self, df: pd.DataFrame, /) -> pd.Index:
105147
def get_invalid_polars(self) -> pl.Expr:
106148
from baybe._optional.polars import polars as pl
107149

108-
return self.condition.to_polars(pl.sum_horizontal(self.parameters)).not_()
150+
weighted = [pl.col(p) * c for p, c in zip(self.parameters, self.coefficients)]
151+
return self.condition.to_polars(pl.sum_horizontal(weighted)).not_()
109152

110153

111154
@define

0 commit comments

Comments
 (0)