designspace.ParamExpr#
- class designspace.ParamExpr(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:
ArithExpr,BoolExpr,VectorExprA parameter, either being declared or being referred to.
ds.param(“x”) returns one of these, and every method on it returns a new one. Nothing is ever mutated, so a partly-built parameter can be shared and branched freely.
The same object plays two roles depending on where it is used. Passed to ds.space(), it declares a parameter. Used inside a constraint or condition, it refers to one, and behaves as an expression: the comparison and arithmetic operators build expression trees rather than computing anything.
ParamExpr is the common base of every builder view. Once a type method has been called you hold a narrower view (RealParamExpr, ChoiceParamExpr, …) exposing only the modifiers valid for that type, but everything here is available throughout.
- prior_spec#
The prior set by .prior() or .log_scale().
- Type:
Any
- quantized_spec#
The grid set by .quantized().
- Type:
QuantizedSpec | None
- default_value#
The value set by .default().
- Type:
Any
- struct_space#
The field space, for a .space().
- Type:
Any
- lift#
Element and count state, once .repeat() has been called.
- Type:
Any
Notes
Every attribute but path is the builder’s accumulated state, not a stable surface: it is what resolution consumes to produce the IR. Read Space.params[path], a ParamDef, for introspection instead.
Examples
Declaring:
>>> lr = ds.param("lr").real(1e-4, 1e-1).log_scale().default(0.01) >>> ds.space(lr).apply_defaults({}) {'lr': 0.01}
Referring:
>>> s = ds.space( ... ds.param("lo").integer(0, 5), ... ds.param("hi").integer(0, 5), ... ).require(ds.param("lo") < ds.param("hi")) >>> s.is_feasible({"lo": 1, "hi": 3}) True
- 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'
- 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']
- 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']
- 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
- 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
- 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
- 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
- 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
- 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
- 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]}
- 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}
- 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']
- 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'}
- 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