designspace.StructParamExpr#
- class designspace.StructParamExpr(path: str, domain: RealDomain | IntegerDomain | CategoricalDomain | OrdinalDomain | BoolDomain | SubsetDomain | PermutationDomain | ChoiceDomain | StructDomain | CustomDomain | SymbolicDomain | CodeDomain | ListDomain | None = None, periodic: bool = False, prior_spec: Any = None, quantized_spec: QuantizedSpec | None = None, default_value: Any = None, condition: BoolExpr | None = None, tags: frozenset[str] = frozenset({}), meta_map: MappingProxyType = <factory>, choice_payloads: MappingProxyType = <factory>, struct_space: Any = None, lift: _ElementSnapshot | None = None)#
Bases:
TypedParamExprA named group of parameters, declared by .space().
Every field is always active together; a struct chooses nothing.
- property children: tuple[Expr, ...]#
The node’s operands, always empty, a reference being a leaf.
Examples
>>> ds.param("x").children () >>> [c.path for c in (ds.param("x") < ds.param("y")).children] ['x', 'y']
- contains(item: Any) BoolExpr#
Whether a subset parameter includes item, as an expression.
- Parameters:
item (Any) – One of the subset’s declared items.
- Returns:
A condition usable in .require(), .forbid(), or .when().
- Return type:
Examples
>>> s = ds.space(ds.param("items").subset(["a", "b", "c"])) >>> s = s.require(ds.param("items").contains("a")) >>> s.is_feasible({"items": ["a", "b"]}) True >>> s.is_feasible({"items": ["b"]}) False
- default(value: Any) Self#
Set the value used to fill this parameter in when it is unset.
Defaults are for completing a configuration, not for repairing one: Space.apply_defaults() fills only what is missing and only where the parameter is active, and it never clamps a value into range.
Position matters around .repeat(). Called before, it sets the default for each element; called after, it sets the default for the list as a whole.
- Parameters:
value (Any) – The fill value. It must be valid for the parameter’s domain.
- Returns:
A new builder with the default set. Last call wins.
- Return type:
Self
Examples
>>> s = ds.space(ds.param("depth").integer(1, 8).default(3)) >>> s.apply_defaults({}) {'depth': 3} >>> s.apply_defaults({"depth": 7}) {'depth': 7}
Element default versus list default:
>>> element = ds.space(ds.param("w").real(0, 1).default(0.5).repeat(3)) >>> element.apply_defaults({}) {'w': [0.5, 0.5, 0.5]} >>> whole = ds.space(ds.param("w").real(0, 1).repeat(3).default([0.1, 0.2, 0.3])) >>> whole.apply_defaults({}) {'w': [0.1, 0.2, 0.3]}
- if_inactive(fallback: object) ArithExpr#
Substitute fallback when this expression has no value.
An expression over an inactive parameter, or an aggregate over a list that is switched off, evaluates to unknown, and a constraint that cannot be decided is treated as inapplicable rather than violated. That is usually right, but sometimes the intended reading is “absent means zero”. This says so.
It substitutes only for inactivity. An expression that is unknown because a value has not been chosen yet stays unknown, and an aggregate over an active but empty list keeps its own empty value, which the fallback would otherwise mask.
- Parameters:
fallback (object) – The value to use when the expression is inactive.
- Returns:
An expression that is never unknown for want of activity.
- Return type:
Examples
Without a fallback the budget cannot be decided, so it does not constrain anything:
>>> s = ds.space( ... ds.param("use_cache").bool(), ... ds.param("cache_mb").integer(64, 512).when(ds.param("use_cache")), ... ds.param("heap_mb").integer(64, 512), ... ) >>> total = ds.param("cache_mb") + ds.param("heap_mb") >>> loose = s.require(total <= 512) >>> loose.is_feasible({"use_cache": False, "heap_mb": 512}) True
With one, an absent cache counts as zero and the rule applies:
>>> guarded = s.require(ds.param("cache_mb").if_inactive(0) + ds.param("heap_mb") <= 400) >>> guarded.is_feasible({"use_cache": False, "heap_mb": 512}) False >>> guarded.is_feasible({"use_cache": False, "heap_mb": 256}) True
- implies(other: BoolExpr) BoolExpr#
Material implication: if this holds, other must too.
The natural shape for a conditional rule such as “if we are on GPU, the batch must be at least 32”, and much clearer than the equivalent ~a | b, which it is exactly (down to the fingerprint).
- Parameters:
other (BoolExpr) – The consequent.
- Returns:
A condition, false only when this holds and other does not.
- Return type:
- Raises:
TypeError – If other is not a boolean expression.
Examples
>>> s = ds.space( ... ds.param("gpu").bool(), ... ds.param("batch").integer(1, 64), ... ).require(ds.param("gpu").implies(ds.param("batch") >= 32)) >>> s.is_feasible({"gpu": True, "batch": 64}) True >>> s.is_feasible({"gpu": True, "batch": 8}) False
The rule says nothing when the antecedent is false:
>>> s.is_feasible({"gpu": False, "batch": 8}) True
- is_active() BoolExpr#
Whether the referenced parameter is active, as a condition.
Lets a constraint ask about presence rather than value: “if the cache is switched on at all, then …”. Distinct from reading the value, which would be unknown for an inactive parameter.
- Returns:
A condition, true when the parameter is present.
- Return type:
Examples
>>> s = ds.space( ... ds.param("use_cache").bool(), ... ds.param("cache_mb").integer(64, 512).when(ds.param("use_cache")), ... ds.param("workers").integer(1, 8), ... ) >>> s = s.require( ... ds.param("cache_mb").is_active().implies(ds.param("workers") <= 4) ... ) >>> s.is_feasible({"use_cache": True, "cache_mb": 128, "workers": 2}) True >>> s.is_feasible({"use_cache": True, "cache_mb": 128, "workers": 8}) False >>> s.is_feasible({"use_cache": False, "workers": 8}) True
- is_in(*values: Any) BoolExpr#
Whether the value is one of values.
The replacement for Python’s in, which cannot be used on an expression: in coerces its result to a bool and would collapse the tree.
- Parameters:
*values (Any) – The values to test membership against.
- Returns:
A condition.
- Return type:
Examples
>>> s = ds.space(ds.param("algo").categorical("a", "b", "c")) >>> s = s.require(ds.param("algo").is_in("a", "b")) >>> s.is_feasible({"algo": "a"}) True >>> s.is_feasible({"algo": "c"}) False
- property kind: str#
The expression node kind, always “ref” for a parameter reference.
Every expression node reports a kind, which is how a consumer walks a constraint tree without isinstance chains.
Examples
>>> ds.param("x").kind 'ref'
- length() ArithExpr#
How many elements a .repeat() list holds, as an expression.
Useful when the count is itself a parameter and you want to constrain the realized length.
- Returns:
An integer-valued expression.
- Return type:
Examples
>>> s = ds.space( ... ds.param("n").integer(1, 3), ... ds.param("w").real(0, 1).repeat(ds.param("n")), ... ).require(ds.param("w").length() >= 2) >>> s.is_feasible({"n": 2, "w": [0.1, 0.2]}) True >>> s.is_feasible({"n": 1, "w": [0.1]}) False
- meta(mapping: dict[str, Any] | None = None, **kwargs: Any) Self#
Attach arbitrary metadata to the parameter.
Metadata is carried through serialization and the fingerprint but never interpreted: units, help text, a UI hint, provenance.
- Parameters:
- Returns:
A new builder carrying the metadata, merged over any already set.
- Return type:
Self
Examples
>>> p = ds.param("timeout").real(0.1, 60.0).meta(unit="seconds") >>> dict(ds.space(p).params["timeout"].meta) {'unit': 'seconds'}
- property params: frozenset[str]#
The parameter paths this expression references.
On a bare reference that is just its own path; on a compound expression it is every path underneath, which is what the dependency graph is built from.
Examples
>>> ds.param("x").params frozenset({'x'}) >>> sorted((ds.param("x") + ds.param("y")).params) ['x', 'y']
- position_of(item: Any) ArithExpr#
Where item sits in a permutation, as a zero-based expression.
- Parameters:
item (Any) – One of the permutation’s declared items.
- Returns:
The item’s index.
- Return type:
Examples
>>> s = ds.space(ds.param("order").permutation(["x", "y", "z"])) >>> s = s.require(ds.param("order").position_of("x") == 0) >>> s.is_feasible({"order": ["x", "y", "z"]}) True >>> s.is_feasible({"order": ["y", "x", "z"]}) False
- prior(dist: Any = None, *, weights: Sequence[float] | None = None) Self#
Set the parameter’s prior, the measure it is sampled from.
A prior is not a hint: it is the coordinate system the parameter lives in. It determines both how the reference sampler draws and how a solver perturbs, which is why there is no separate “transform” concept. Pass a distribution for a numeric parameter, or weights= for a categorical or choice.
- Parameters:
dist (Any) – A prior for a numeric parameter: ds.Log(), ds.Logit(), ds.Power(p), or any object implementing the external-prior protocol (a ppf, optionally a cdf). Mutually exclusive with weights.
weights (Sequence[float] | None) – Relative weights, one per declared value or variant, for a categorical, ordinal, or choice parameter. Need not sum to 1.
- Returns:
A new builder with the prior set. Last call wins.
- Return type:
Self
- Raises:
ResolutionError – If neither or both of dist and weights are given.
Examples
>>> s = ds.space( ... ds.param("algo").categorical("greedy", "exact").prior(weights=[9, 1]), ... ) >>> s.sample_dicts(4, seed=0) [{'algo': 'greedy'}, {'algo': 'greedy'}, {'algo': 'greedy'}, {'algo': 'greedy'}]
.log_scale() is shorthand for the equivalent Log prior:
>>> a = ds.space(ds.param("lr").real(1e-4, 1.0).log_scale()) >>> b = ds.space(ds.param("lr").real(1e-4, 1.0).prior(ds.Log())) >>> a.fingerprint() == b.fingerprint() True
- prop(name: str) Prop#
Read a named property of a .custom() value, as an expression.
A custom type’s values are opaque to the library, so this is the window into them: the type’s properties() supplies the named quantities, and constraints can then be written over those. The result is dual-typed, usable as a number or as a condition, depending on what the property returns.
- Parameters:
name (str) – A property name the custom type reports.
- Returns:
An expression reading that property.
- Return type:
Examples
>>> class GridType: ... type_key = "grid" ... def validate(self, v): return v["n"] >= 1 ... def to_json(self, v): return v ... def from_json(self, d): return d ... def describe(self): return {"kind": "grid"} ... def properties(self): return {"cells": int} ... def extract(self, v, prop): return v["n"] * v["n"] >>> s = ds.space(ds.param("g").custom(GridType())) >>> s = s.require(ds.param("g").prop("cells") <= 4) >>> s.is_feasible({"g": {"n": 2}}) True >>> s.is_feasible({"g": {"n": 3}}) False
- repeat(*counts: int | ArithExpr) ListParamExpr#
Lift the parameter to a list of independent copies.
Each element is drawn independently from the declared domain. The count may be a literal or an expression over another parameter, in which case the list’s length varies from draw to draw.
Several counts read as a shape: .repeat(2, 3) gives two rows of three, the first count outermost.
- Parameters:
*counts (int | ArithExpr) – One count per nesting level, outermost first. At least one.
- Returns:
The list-typed builder. Call .repeat() again to nest further.
- Return type:
- Raises:
ResolutionError – If no count is given.
Examples
>>> ds.space(ds.param("w").real(0, 1).repeat(3)).sample_one(seed=0) {'w': [0.6369616873214543, 0.2697867137638703, 0.04097352393619469]}
A shape:
>>> ds.space(ds.param("g").real(0, 1).repeat(2, 2)).sample_one(seed=0) {'g': [[0.6369616873214543, 0.2697867137638703], [0.04097352393619469, 0.016527635528529094]]}
A parameter-driven count:
>>> s = ds.space( ... ds.param("n").integer(1, 3), ... ds.param("w").real(0, 1).repeat(ds.param("n")), ... ) >>> s.sample_one(seed=0) {'n': 2, 'w': [0.2697867137638703, 0.04097352393619469]}
- size() ArithExpr#
How many items a subset holds, as an expression.
- Returns:
An integer-valued expression.
- Return type:
Examples
>>> s = ds.space(ds.param("items").subset(["a", "b", "c"])) >>> s = s.require(ds.param("items").size() >= 2) >>> s.is_feasible({"items": ["a", "b"]}) True >>> s.is_feasible({"items": ["a"]}) False
- sum_over(mapping: dict[Any, float]) ArithExpr#
Total a per-item weight over a subset’s members, as an expression.
The natural way to write a budget: give each item a cost, then constrain the total of whichever items are selected.
- Parameters:
- Returns:
The sum of mapping[i] over the selected items i.
- Return type:
Examples
>>> s = ds.space(ds.param("items").subset(["a", "b", "c"])) >>> cost = ds.param("items").sum_over({"a": 1.0, "b": 2.0, "c": 3.0}) >>> s = s.require(cost <= 3.0) >>> s.is_feasible({"items": ["a", "b"]}) True >>> s.is_feasible({"items": ["b", "c"]}) False
- tag(*tags: str) Self#
Attach labels to the parameter.
Tags are how you address groups of parameters later, with .filter() selects by them. They carry no meaning to the library.
- Parameters:
*tags (str) – Labels to add. Repeated calls accumulate.
- Returns:
A new builder carrying the tags.
- Return type:
Self
Examples
>>> s = ds.space( ... ds.param("lr").real(1e-4, 1.0).tag("optimizer"), ... ds.param("batch").integer(8, 64), ... ) >>> list(s.filter(("optimizer",)).params) ['lr']
- when(condition: BoolExpr) Self#
Make the parameter active only when condition holds.
This is how a design space branches. An inactive parameter is absent from the configuration dict entirely, not None and not a placeholder, so a config always says exactly what applies to it.
Calling .when() more than once accumulates: the conditions are combined with and, in call order.
- Parameters:
condition (BoolExpr) – A boolean expression over other parameters. A bool parameter can be used directly, without comparing it to True.
- Returns:
A new builder carrying the condition.
- Return type:
Self
- Raises:
TypeError – If condition is not a boolean expression. In particular Python’s and/or/in cannot be used, since they would coerce the expression to a bool. Use &, |, ~, and .is_in() instead.
Examples
>>> s = ds.space( ... ds.param("use_cache").bool(), ... ds.param("cache_mb").integer(64, 512).when(ds.param("use_cache")), ... ) >>> s.sample_one(seed=0) {'use_cache': False} >>> s.sample_one(seed=2) {'use_cache': True, 'cache_mb': 198}