SearchSpace¶
Variable definitions and linear constraints.
search_space
¶
SearchSpace
¶
Class for storing and managing the search space in a consistent way across backends. Provides methods for conversions to different formats required by different backends.
add_variable
¶
add_variable(name: str, var_type: str, role: Optional[str] = None, changes: Optional[str] = None, **kwargs)
Add a variable to the search space.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Variable name |
required |
var_type
|
str
|
"real", "integer", "categorical", "discrete", or "context" |
required |
role
|
Optional[str]
|
"controllable" (default) or "observed". An observed variable is measured, never optimized or sampled by DoE, and must be supplied with every experiment and every acquisition call. |
None
|
changes
|
Optional[str]
|
"easy" (default), "hard" or "very_hard"; controllable only. Drives split-plot design generation. |
None
|
**kwargs
|
Additional parameters: - real/integer: min, max - categorical: values (list of strings) - discrete: allowed_values (list of numbers, at least 2, no duplicates) - context: no additional parameters required |
{}
|
Raises:
| Type | Description |
|---|---|
ValueError
|
for a duplicate name, a reserved split-plot name
( |
Example
A legacy var_type="context" is read as an observed real.
get_dimension_names
¶
Variable names positionally paired with self.skopt_dimensions.
get_dimension_names()[i] is the name of skopt_dimensions[i], so
this is what any caller zipping a sampled point against variable names
wants -- a sample drawn from skopt_dimensions has one value per
dimension, not one per variable.
Derived from self.variables rather than by reading dim.name off
each dimension, deliberately: this states which variables should hold
a dimension and in what order, so it remains the correct answer to
compare a corrupted skopt_dimensions against rather than agreeing
with it.
get_dimension_variables
¶
The variable definitions positionally paired with skopt_dimensions.
The same list :meth:get_dimension_names names, projected to the full
variable dicts instead of the names -- one traversal, one rule
(:attr:_DIMENSION_BEARING_TYPES), two projections.
Callers that need the names to key a sampled point want
:meth:get_dimension_names. This is for the ones that need the
definitions themselves and, critically, need their positions: the
classical designs and the optimal-design candidate pipeline build a
coded matrix with one column group per dimension-bearing variable and
then index it by a variable's position. Enumerating self.variables
for that position is the defect described above -- it also reaches for
var['min'] on a context variable, which has no bounds.
get_dimension_index
¶
Index into self.skopt_dimensions for variable name.
None when name carries no dimension -- either because it is not
registered at all, or because it is registered as a type that has none
(context). Both answers are "there is no dimension slot for this
name", which is the only thing a caller indexing skopt_dimensions
can act on; callers that need to tell a missing variable from a context
one look at self.variables.
replace_variable
¶
Redefine an already-registered variable in place, keeping its position.
Same signature as add_variable -- name, var_type, and the
type's own kwargs -- so a caller that has translated a payload once can
hand it to either without a second translation table to drift.
The new definition is built by calling add_variable on a throwaway
space. That is the whole point of this method rather than an
incidental way to write it: there is no second construction path, so
every guard add_variable enforces -- the finite-bound check, the
float64 span check, the empty-categorical and short-discrete checks,
and any guard added after this was written -- applies to a redefinition
for free. The prior implementations of this operation were hand-rolled
copies of add_variable's branches with the guards omitted, which is
exactly the rot this shape exists to prevent.
The throwaway starts empty, so the one guard that must not fire here -- the duplicate-name check -- does not, without needing a flag to suppress it.
It also makes the operation atomic. Everything that can fail happens on
the throwaway before self is touched, so a rejected redefinition
leaves the variable exactly as it was rather than half-overwritten.
Ordering is preserved in both lists, which is why this is not
remove_variable followed by add_variable: that pair moves the
variable to the end of both, and skopt_dimensions' positional
pairing makes the reordering observable to every consumer.
Type transitions are handled in both directions, including to and from
context: a variable that gains a dimension has one inserted at the
position its name maps to, and one that loses its dimension has it
removed.
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
remove_variable
¶
Remove name and the dimension it owns, if it owns one.
Every other variable keeps its position in both lists, and the pairing
between them survives. Removing a context variable removes no
dimension at all -- the case a single index popped from both lists gets
wrong, by discarding some other variable's dimension.
Linear constraints whose coefficients name name are dropped, with
a warning naming each one (A14): filter_feasible sums only the
columns present, so a kept x1 + x2 <= 12 would otherwise be
enforced as x1 <= 12 after x2 is gone. Derived variables that
reference name are left alone.
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
from_dict
¶
Load search space from a list of dictionaries (used with JSON/CSV loading).
to_skopt
¶
Get skopt dimensions for scikit-learn.
to_ax_space
¶
Convert to Ax parameter format (controllable variables only).
to_botorch_bounds
¶
Create bounds in BoTorch format.
For discrete variables, bounds span [min(allowed_values), max(allowed_values)]. The acquisition optimizer uses these as the continuous relaxation bounds; the discrete constraint is enforced separately via optimize_acqf_mixed. Observed variables are never optimized and have no entry.
get_variable_names
¶
All variable names, controllable first, then observed (the model's column order).
get_controllable_variable_names
¶
Names of controllable variables in registration order.
get_observed_variable_names
¶
Names of observed variables (including legacy context) in registration order.
get_tunable_variable_names
¶
Alias of :meth:get_controllable_variable_names.
get_context_variable_names
¶
Alias of :meth:get_observed_variable_names.
get_variables_by_changes
¶
Controllable variable dicts whose change difficulty is level.
get_categorical_variables
¶
Get list of categorical variable names.
check_categorical_values
¶
Raise ValueError if a categorical variable holds an undeclared value.
data is one row (a dict) or a DataFrame. A value matches a declared
level by value first and by string form second, the rule the BoTorch
model encodes with, so a level declared as "1" accepts the integer
1 from a CSV. Missing values (None/NaN) and absent columns are skipped:
missing variables are reported elsewhere. Applies to controllable and
observed categoricals alike.
add_derived_variable
¶
Register a derived (non-tunable) variable.
Derived variables are deterministic functions of existing input variables. They are appended to the GP feature matrix at train and predict time, but the acquisition function never suggests values for them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Column name for the derived feature. |
required |
func
|
Callable with signature |
required | |
input_cols
|
List[str]
|
Base variable names this feature depends on (for documentation; the full row dict is still passed to func). |
required |
description
|
str
|
Human-readable description stored in session JSON. |
''
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If name conflicts with an existing tunable variable or an already-registered derived variable. |
register_derived_variable
¶
Re-attach a callable to a derived variable stub after session load.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Name of the derived variable to update. |
required |
func
|
Callable with signature |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no derived variable with the given name exists. |
add_derived_variable_stub
¶
Restore a derived variable stub from session JSON (func=None).
has_derived_variables
¶
Return True if any derived variables are registered.
get_derived_variable_names
¶
Return list of derived variable names (in registration order).
derived_variables_to_dict
¶
Return serializable metadata for all derived variables (no func).
from_json
classmethod
¶
Class method to create a SearchSpace from a JSON file.
add_constraint
¶
add_constraint(constraint_type: str, coefficients: Dict[str, float], rhs: float, name: Optional[str] = None)
Add linear input constraint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
constraint_type
|
str
|
'inequality' (sum(coeff_i * x_i) <= rhs) or 'equality' (sum(coeff_i * x_i) == rhs) |
required |
coefficients
|
Dict[str, float]
|
{variable_name: coefficient} mapping |
required |
rhs
|
float
|
right-hand side value |
required |
name
|
Optional[str]
|
optional human-readable name. Auto-generated as
|
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
unknown constraint_type, a coefficient variable that is
missing or non-numeric, a non-numeric or non-finite rhs or
coefficient, or a duplicate explicit name. Never TypeError:
callers such as the API router catch only ValueError, so a
|
restore_constraints
¶
Install constraints read from a file, replacing what is registered.
The single entry point for both file loaders --
:meth:load_from_json (the desktop loader) and
OptimizationSession.load_session (POST /sessions/upload). Both
used to assign self.constraints = data.get('constraints', [])
straight across, which is why an uploaded session could install
entries that POST /constraints and /variables/load both refuse.
Shape is validated; registration is not re-run. Those are separable
and only the second one is unsafe here. Re-registering through
:meth:add_constraint would re-apply its reference checks -- the
variable exists, the variable is numeric, the name is unused -- and a
session can legitimately reach a state that fails them:
:meth:remove_variable documents that it leaves referencing
constraints alone, and DELETE /variables/{name} calls it, so a live
session can hold, and therefore save, a constraint naming a variable
that is gone. Refusing to load such a file would not repair the
constraint; it would put the whole session out of reach. Duplicate
names survive for the same reason, and DELETE /constraints/{name}
already documents removing the first match only because of it.
None of that argument reaches the shape checks in
:meth:_validate_constraint_shape, which ask nothing about the search
space. A "rhs": null entry is not a state any live session was ever
in, so nothing is being made unloadable by refusing it -- while
carrying it across made GET /constraints report it verbatim, every
design a 500, and, once a name key was missing,
DELETE /constraints/{name} a 500 for every constraint in the
session rather than only the malformed one. That is the whole repair
route, so the session was a dead end through the API.
A missing or None name is filled with
:meth:_next_auto_constraint_name rather than refused: name is
optional on the way in (add_constraint auto-generates it, and
/variables/load accepts an entry without one), so a file lacking it
is under-specified, not invalid. Filling it is what makes every entry
addressable by the delete route.
Entries are copied, coefficients included, so the installed constraints do not alias the parsed document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raw
|
Any
|
the file's |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
filter_feasible
¶
Boolean mask: which rows satisfy ALL registered linear input constraints.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
pandas DataFrame (columns are variable names) or a list of dicts. |
required | |
rtol
|
float
|
Relative tolerance; see |
0.001
|
atol
|
float
|
Absolute tolerance. Equality is feasible when |lhs - rhs| <= atol + rtol * max(|rhs|, scale); inequality when lhs <= rhs + atol + rtol * max(|rhs|, scale). |
1e-06
|
Returns:
| Type | Description |
|---|---|
ndarray
|
numpy boolean array of length len(points). All True if no constraints. |
is_feasible
¶
Whether a single point (dict or 1-row DataFrame) is feasible.
to_botorch_constraints
¶
Convert to BoTorch format for optimize_acqf.
Each constraint is a tuple (indices_tensor, coefficients_tensor, rhs_float).
ALchemist convention (user-facing): inequality means sum(coeff_i * x_i) <= rhs
BoTorch convention (optimize_acqf): inequality means
sum(coeff_i * x_i) >= rhs
To convert, we negate both the coefficients and the rhs for inequality constraints. Equality constraints are sign-symmetric and are passed through unchanged.
Coordinate space: constraints are emitted in raw variable space,
matching the bounds passed to optimize_acqf. BoTorchModel applies
its Normalize input transform internally, so the acquisition
optimizer operates on raw-scale variables and returns raw-scale
candidates. The stored constraints are already raw-scale, so the
coefficients and rhs pass through unchanged (only the inequality sign is
flipped for BoTorch's >= convention).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
feature_names
|
List[str]
|
ordered list of feature column names matching model input |
required |
Returns:
| Type | Description |
|---|---|
Optional[List]
|
(inequality_constraints, equality_constraints) — each is a list of tuples |
Optional[List]
|
or None if no constraints of that type exist. |
role_of
¶
'observed' for an observed (or legacy context) variable, else 'controllable'.
kind_of
¶
The value kind: real, integer, discrete or categorical.
changes_of
¶
Change difficulty of a controllable variable; None for an observed one.