Predicate transparency#
The way a predicate is written decides how much of the library’s machinery can act on it. All three forms below are one line and all three are enforced correctly, so the cost is invisible at the call site.
transparency |
form |
margins |
|
tighten-not-reject |
|---|---|---|---|---|
white box |
an expression over parameter values |
yes |
yes |
yes (bound-origin) |
grey box |
opaque scalar under a structural comparison |
yes |
no |
no |
black box |
an opaque predicate |
no |
no |
no |
White box#
The predicate is an expression the library can read, and every facility applies.
>>> import designspace as ds
>>> space = ds.space(
... ds.param("a").integer(0, 10),
... ds.param("b").integer(0, 10),
... ).require(ds.param("b") <= ds.param("a"))
>>> [e.margin for e in space.evaluate_constraints({"a": 7, "b": 2})]
[5.0]
A margin reports how far from the boundary a configuration sits, rather than only whether it is legal, which is the signal a solver follows downhill. Because the structure is visible, the space can also report what remains available for one parameter given the others:
>>> space.remaining_domain("b", {"a": 4})
IntegerRemaining(lo=0, hi=4, grid=None)
Grey box#
The value is opaque but arrives as a number, and the comparison against it is structural. The comparison stays visible even though the computation does not.
>>> cost = ds.value(lambda x: x * 2.0, ds.param("x"), returns=float)
>>> space = ds.space(ds.param("x").integer(0, 10)).require(cost <= 8.0)
>>> [e.margin for e in space.evaluate_constraints({"x": 3})]
[2.0]
The margin survives because subtracting 6.0 from 8.0 requires no
understanding of lambda x: x * 2.0. Narrowing a domain does require it, since
that would mean inverting the opaque part:
>>> space.remaining_domain("x", {})
IntegerRemaining(lo=0, hi=10, grid=None)
Black box#
The predicate itself is opaque. The library can only call it and take the answer.
>>> is_even = ds.value(lambda x: x % 2 == 0, ds.param("x"), returns=bool)
>>> space = ds.space(ds.param("x").integer(0, 10)).require(is_even)
>>> [e.margin for e in space.evaluate_constraints({"x": 3})]
[None]
margin is None because there is no boundary to measure a distance to. The
constraint is still enforced; it behaves as a wall rather than a slope.
Rationale#
The argument for transparency is not solver consumption. A solver facing a black-box objective is not handing these constraints to a MIP or CP solver anyway.
Margins, evaluate_partial, remaining_domain and bound-origin tightening are
all designspace’s own machinery, and all of them run on structure. A
black-box predicate switches them off for that constraint, inside a library
otherwise being paid for.
Tightening instead of rejecting#
A grey predicate is usually within reach where a black one gets written out of habit, because anything physical has a numeric value:
>>> black = ds.value(lambda w: w <= 5.0, ds.param("weight_g"), returns=bool)
>>> grey = ds.value(lambda w: w, ds.param("weight_g"), returns=float) <= 5.0
The two accept identical configurations. The second keeps the margin the first discards, at the cost of one line of rewriting, done once, at declaration time.