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

remaining_domain narrowing

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.