designspace.FreshParamExpr#
- class designspace.FreshParamExpr(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:
ParamExprWhat ds.param(name) returns: a parameter with no type chosen yet.
The nine type methods here are the fork in the road. Calling one says what kind of value the parameter holds and narrows the builder to the matching view, which offers only the modifiers valid for that type. Calling a second one is an error, caught statically by the type checker and at resolution either way.
Examples
>>> ds.param("lr").real(1e-4, 1e-1) RealParamExpr(path='lr'...) >>> ds.param("algo").categorical("greedy", "exact") CategoricalParamExpr(path='algo'...)
- real(lo: float | ArithExpr, hi: float | ArithExpr, periodic: bool = False) RealParamExpr#
Declare a continuous parameter on [lo, hi].
Bounds may be expressions over other parameters, which makes the domain depend on what has already been chosen.
- Parameters:
- Returns:
The narrowed builder.
- Return type:
Examples
>>> s = ds.space(ds.param("lr").real(1e-4, 1e-1).log_scale()) >>> round(s.sample_one(seed=0)["lr"], 6) 0.008145
A bound can reference another parameter:
>>> s = ds.space( ... ds.param("lo").real(0.0, 1.0), ... ds.param("hi").real(ds.param("lo"), 1.0), ... ) >>> c = s.sample_one(seed=0) >>> c["lo"] <= c["hi"] True
- integer(lo: int | ArithExpr, hi: int | ArithExpr) IntegerParamExpr#
Declare an integer parameter on [lo, hi], both inclusive.
- Parameters:
- Returns:
The narrowed builder.
- Return type:
Examples
>>> s = ds.space(ds.param("depth").integer(1, 4)) >>> s.cardinality() 4 >>> s.sample_one(seed=0) {'depth': 3}
- categorical(*values: Any) CategoricalParamExpr#
Declare an unordered choice among values.
Use this when the values have no meaningful order: solver names, kernel types, strategies. If they are ordered, use .ordinal(), which lets comparisons work. If a value needs parameters of its own, use .choice().
Values are compared with type-tagged equality, so 1 and 1.0 are distinct.
- Parameters:
*values (Any) – The allowed values. At least one; duplicates are an error.
- Returns:
The narrowed builder.
- Return type:
Examples
>>> s = ds.space(ds.param("algo").categorical("greedy", "exact")) >>> s.sample_one(seed=0) {'algo': 'exact'}
- ordinal(*values: Any) OrdinalParamExpr#
Declare an ordered choice among values.
Order is declaration position, not the values’ natural order, so “low”, “medium”, “high” compare as you would want them to. That is what distinguishes this from .categorical(): comparison operators work.
- Parameters:
*values (Any) – The allowed values, in increasing order.
- Returns:
The narrowed builder.
- Return type:
Examples
>>> s = ds.space(ds.param("effort").ordinal("low", "medium", "high")) >>> s = s.require(ds.param("effort") >= "medium") >>> s.is_feasible({"effort": "high"}) True >>> s.is_feasible({"effort": "low"}) False
- bool() BoolParamExpr#
Declare a boolean parameter.
A bool parameter doubles as a condition, so it can be passed straight to .when() without comparing it to True.
- Returns:
The narrowed builder, which is also a boolean expression.
- Return type:
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}
- subset(items: Sequence[Any], min_size: int = 0, max_size: int | None = None) SubsetParamExpr#
Declare a selection of any number of items.
Set semantics: order does not matter and there are no duplicates. Query it with .contains(), .size(), and .sum_over().
- Parameters:
- Returns:
The narrowed builder.
- Return type:
Examples
>>> s = ds.space(ds.param("passes").subset(["inline", "unroll", "vectorize"])) >>> s.sample_one(seed=0) {'passes': ['unroll', 'vectorize']} >>> s.cardinality() 8
- permutation(items: Sequence[Any]) PermutationParamExpr#
Declare an ordering of all of items.
Every item appears exactly once; only the order varies. Query a position with .position_of().
- Parameters:
items (Sequence[Any]) – The items to order.
- Returns:
The narrowed builder.
- Return type:
Examples
>>> s = ds.space(ds.param("order").permutation(["x", "y", "z"])) >>> s.sample_one(seed=0) {'order': ['z', 'x', 'y']} >>> s.cardinality() 6
- choice(*variants: str | tuple[str, Any], **keyword_variants: Any) ChoiceParamExpr#
Declare alternatives, each optionally carrying its own parameters.
This is how a design space branches structurally: pick a variant, and that variant’s parameters become active while the others’ vanish from the config. Use it over .categorical() when the alternatives are not interchangeable, when each brings its own knobs.
Three spellings, mixable in one call: a bare string for a variant with no payload, a (name, space) tuple, or name=space as a keyword. Use the tuple form when the name is not a valid Python identifier.
- Parameters:
- Returns:
The narrowed builder.
- Return type:
Examples
>>> s = ds.space( ... ds.param("opt").choice( ... "adagrad", ... sgd=ds.space(ds.param("momentum").real(0.0, 1.0)), ... ), ... ) >>> list(s.params) ['opt', 'opt.sgd.momentum']
A bare variant is just its name; a variant with a payload nests:
>>> s.sample_one(seed=2) {'opt': 'adagrad'} >>> s.sample_one(seed=0) {'opt': {'sgd': {'momentum': 0.2697867137638703}}}
Note that .params uses flat definition paths while a config nests. ds.flatten() and ds.unflatten() convert between the two.
- custom(param_type: ParamType | None = None, sampler: Callable[[Any], Any] | None = None, validator: Callable[[Any], bool] | None = None) CustomParamExpr#
Declare a parameter whose values are of your own type.
The extension point for structure the built-in types cannot express: a graph, a topology, a schedule with a global invariant. Reach for it when an invariant is genuinely global (connectivity, pairwise spacing) or when expressing the structure with primitives would leave rejection sampling doing all the work.
Two forms, and exactly one may be used. Passing param_type gives the full protocol: the type can serialize, describe itself, expose properties to .prop(), and optionally sample. Passing sampler and validator is a callback shorthand for quick work, and it cannot be serialized or fingerprinted.
- Parameters:
param_type (ParamType | None) – An object implementing the ParamType protocol. Mutually exclusive with sampler/validator.
sampler (Callable[[Any], Any] | None) – Shorthand form: called with a numpy generator, returns a value. Must be given together with validator.
validator (Callable[[Any], bool] | None) – Shorthand form: returns whether a value is acceptable.
- Returns:
The narrowed builder.
- Return type:
- Raises:
ResolutionError – If both forms are given, neither is, or the shorthand form is missing one of its two callbacks.
Examples
>>> class Weekday: ... type_key = "weekday" ... names = ["mon", "tue", "wed"] ... def validate(self, v): return v in self.names ... def to_json(self, v): return v ... def from_json(self, d): return d ... def describe(self): return {"names": self.names} ... def sample(self, rng): return self.names[int(rng.integers(0, 3))] >>> s = ds.space(ds.param("day").custom(Weekday())) >>> s.sample_one(seed=0) {'day': 'wed'} >>> s.validate({"day": "sun"}).valid False
The shorthand, for throwaway work:
>>> quick = ds.param("u").custom( ... sampler=lambda rng: float(rng.random()), ... validator=lambda v: 0.0 <= v <= 1.0, ... ) >>> ds.space(quick).validate({"u": 0.5}).valid True
- symbolic(signature: Signature, primitives: Sequence[str | Primitive | FloatLiteral | IntLiteral], max_depth: int, validators: Sequence[Callable[[Any], bool]] | None = None, sampler: Callable[[Any], Any] | None = None) SymbolicParamExpr#
Declare a parameter holding a symbolic expression tree.
For design spaces whose subject is a formula: an acquisition function, a cooling schedule, a heuristic. The library declares and validates the tree’s shape but neither generates nor evaluates it: tree search is a solver’s job, and evaluation is your interpreter’s. Primitive names are declared metadata; nothing here calls them.
Values are dicts of the form {“ast”: …, “source”: …}, where the AST is built from {“op”, “args”}, {“var”}, and {“const”} nodes. Variables come from signature.args.
The parameter is non-generative unless sampler is given.
- Parameters:
signature (Signature) – Argument names and types, and the return type. The argument names become the usable variables.
primitives (Sequence[str | Primitive | FloatLiteral | IntLiteral]) – The vocabulary. A bare string names an operator with unchecked arity; a ds.Primitive declares arity so it can be checked; a ds.FloatLiteral/ds.IntLiteral admits constants in a range.
max_depth (int) – Maximum tree depth.
validators (Sequence[Callable[[Any], bool]] | None) – Extra checks run against the tree. Not serializable.
sampler (Callable[[Any], Any] | None) – Makes the parameter generative. Not serializable.
- Returns:
The narrowed builder.
- Return type:
Examples
>>> sig = ds.Signature(args={"x": float}, returns=float) >>> f = ds.param("f").symbolic( ... sig, ... primitives=["add", "mul", ds.FloatLiteral(-1.0, 1.0)], ... max_depth=3, ... ) >>> s = ds.space(f) >>> tree = {"ast": {"op": "add", "args": [{"var": "x"}, {"const": 0.5}]}} >>> s.validate({"f": tree}).valid True
An operator outside the declared vocabulary is rejected:
>>> s.validate({"f": {"ast": {"op": "cos", "args": [{"var": "x"}]}}}).valid False >>> s.has_nongenerative_params True
- code(signature: Signature, description: str = '', constraints: Sequence[str] | None = None, examples: Sequence[Any] | None = None, validators: Sequence[Callable[[str], bool]] | None = None) CodeParamExpr#
Declare a parameter holding freeform source code.
For a design space with a slot an external process fills: a human, a code-generating model, a library of hand-written implementations. The library carries and validates the source; it never writes or runs it, so this parameter is always non-generative and a space containing one cannot be sampled unless the parameter is defaulted, frozen, or inactive.
description, constraints, and examples are declared metadata: serialized and fingerprinted, never interpreted. They are there for whatever backend does the filling.
- Parameters:
signature (Signature) – The interface the source must implement.
description (str) – What the code should do, in prose.
constraints (Sequence[str] | None) – Additional requirements, in prose.
examples (Sequence[Any] | None) – Example implementations or input/output pairs.
validators (Sequence[Callable[[str], bool]] | None) – Checks run against the source text. Not serializable.
- Returns:
The narrowed builder.
- Return type:
Examples
>>> sig = ds.Signature(args={"x": float}, returns=float) >>> impl = ds.param("impl").code(sig, description="a fitness function") >>> s = ds.space(impl) >>> s.validate({"impl": {"source": "def f(x): return x * x"}}).valid True >>> s.has_nongenerative_params True
- space(*exprs: Any) StructParamExpr#
Declare a struct: a named group of parameters, always active together.
Use this for pure grouping, when several parameters belong to one another and you want them namespaced. Unlike .choice(), a struct picks nothing: every field is always present.
Pass a prebuilt Space instead of loose parameters when the group needs its own constraints, which is the only way to attach per-element constraints to a repeated struct.
- Parameters:
*exprs (Any) – The field builders, or a single prebuilt Space.
- Returns:
The narrowed builder.
- Return type:
Examples
>>> s = ds.space( ... ds.param("pid").space( ... ds.param("kp").real(0.0, 1.0), ... ds.param("ki").real(0.0, 1.0), ... ), ... ) >>> list(s.params) ['pid', 'pid.kp', 'pid.ki'] >>> s.sample_one(seed=0) {'pid': {'kp': 0.6369616873214543, 'ki': 0.2697867137638703}}
With its own constraint, via a prebuilt space:
>>> inner = ds.space( ... ds.param("lo").real(0.0, 1.0), ... ds.param("hi").real(0.0, 1.0), ... ).require(ds.param("lo") < ds.param("hi")) >>> s = ds.space(ds.param("band").space(inner)) >>> s.is_feasible({"band": {"lo": 0.2, "hi": 0.8}}) True >>> s.is_feasible({"band": {"lo": 0.8, "hi": 0.2}}) False
- 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
- 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}