Sampling and validation#
A declared space answers two questions: what configurations does it produce, and is a given configuration one of them. This page continues with the simulated annealing space from declaring a space.
import designspace as ds
space = ds.space(
ds.param("initial_temp").real(1e-2, 1e3).log_scale(),
ds.param("min_temp").real(1e-4, 1.0).log_scale(),
ds.param("cooling_rate").real(0.80, 0.999).quantized(step=0.005),
ds.param("steps_per_temp").integer(1, 500),
ds.param("neighborhood").categorical("swap", "insert", "reverse"),
ds.param("acceptance").ordinal("greedy", "boltzmann", "metropolis"),
).forbid(ds.param("min_temp") >= ds.param("initial_temp"))
print(space)
Space: 6 params, 0 conditional, 1 constraints
initial_temp real [0.01, 1000.0] log
min_temp real [0.0001, 1.0] log
cooling_rate real [0.8, 0.999] step=0.005
steps_per_temp integer [1, 500]
neighborhood categorical {'swap', 'insert', 'reverse'}
acceptance ordinal ('greedy' < 'boltzmann' < 'metropolis')
forbid min_temp >= initial_temp
One draw#
sample_one returns a plain nested dict. The seed makes it reproducible.
config = space.sample_one(seed=0)
config
{'initial_temp': 15.304122616866628,
'min_temp': 0.0011999049779393507,
'cooling_rate': 0.805,
'steps_per_temp': 9,
'neighborhood': 'reverse',
'acceptance': 'metropolis'}
A config can be pretty-printed against its space using pretty, showing each
value beside the domain it satisfies.
print(ds.pretty(config, space))
Config: 6 params, 6 set, 0 inactive, valid
initial_temp = 15.304122616866628 in [0.01, 1000.0]
min_temp = 0.0011999049779393507 in [0.0001, 1.0]
cooling_rate = 0.805 in [0.8, 0.999]
steps_per_temp = 9 in [1, 500]
neighborhood = 'reverse' in {'swap', 'insert', 'reverse'}
acceptance = 'metropolis' in ('greedy' < 'boltzmann' < 'metropolis')
forbid min_temp >= initial_temp ok margin -15.303
cooling_rate lands on the declared grid, and the two temperatures respect the
forbid, because the sampler rejects any draw that trips a hard constraint.
config["min_temp"] < config["initial_temp"]
True
Many draws#
sample_dicts returns a list of the same shape.
batch = space.sample_dicts(5, seed=1)
[c["neighborhood"] for c in batch]
['swap', 'reverse', 'swap', 'insert', 'swap']
Since the forbid holds by construction, it holds across the whole batch:
assert all(c["min_temp"] < c["initial_temp"] for c in space.sample_dicts(200, seed=2))
Validation#
validate checks a configuration against the space and returns a
ValidationResult rather than raising.
result = space.validate(config)
result.valid
True
A configuration outside a domain reports which parameter and why. Nothing is clamped: an out-of-range value is an error, never a rounded input.
bad = dict(config, steps_per_temp=9999)
space.validate(bad).param_errors
(steps_per_temp: out_of_bounds (value=9999),)
validate_param checks a single value without needing a whole configuration:
space.validate_param("acceptance", "simulated").param_errors
(acceptance: out_of_bounds (value='simulated'),)
Feasibility#
validate covers two separate things: whether each value is legal for its
parameter, and whether the constraints hold. valid is the conjunction, so it
is false if either fails, and the two halves of the result are what tell them
apart.
# Both values sit inside their own domains; it is the pair that is illegal.
infeasible = dict(config, initial_temp=0.05, min_temp=0.5)
result = space.validate(infeasible)
result.valid, result.param_errors
(False, ())
Every value is in range, so param_errors is empty; the forbid is what makes
valid false. is_feasible asks only the constraint half of that question:
space.is_feasible(infeasible)
False
# A malformed config and a well-formed but infeasible one both report
# valid=False, and `param_errors` is what distinguishes them.
assert space.validate(infeasible).param_errors == ()
assert space.validate(bad).param_errors != ()
infeasibility_reasons names what failed:
space.infeasibility_reasons(infeasible)
['forbid violated (margin=0.45): min_temp >= initial_temp']
The flattened view#
A configuration is nested, matching the declaration. ds.flatten re-keys it by
instance path, which is the grammar DataFrame columns, expression references and
error messages all use.
ds.flatten(config, space)
{'initial_temp': 15.304122616866628,
'min_temp': 0.0011999049779393507,
'cooling_rate': 0.805,
'steps_per_temp': 9,
'neighborhood': 'reverse',
'acceptance': 'metropolis'}
ds.unflatten reverses it exactly:
ds.unflatten(ds.flatten(config, space), space) == config
True
Where to go next#
Conditionality and structure makes the set of parameters depend on the values drawn.