Skip to content

SearchSpace

Variable definitions and linear constraints.

search_space

SearchSpace

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 (WholePlot, Subplot, RunOrder), an unknown role or change difficulty, or changes on an observed variable.

Example
from alchemist_core.data.search_space import SearchSpace

space = SearchSpace()
space.add_variable("x1", "real", min=0.0, max=1.0)
space.add_variable("h1", "real", min=0.0, max=10.0, changes="hard")
space.add_variable("k", "categorical", values=["p", "q"], role="observed")

A legacy var_type="context" is read as an observed real.

get_dimension_names

get_dimension_names() -> List[str]

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

get_dimension_variables() -> List[Dict[str, Any]]

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

get_dimension_index(name: str) -> Optional[int]

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

replace_variable(name: str, var_type: str, **kwargs)

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 name is not registered, if the new definition fails any of add_variable's guards, or if the new type is one a linear constraint may not reference (:attr:_CONSTRAINT_NUMERIC_TYPES) while a registered constraint names name in its coefficients.

remove_variable

remove_variable(name: str)

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 name is not registered.

from_dict

from_dict(data: List[Dict[str, Any]])

Load search space from a list of dictionaries (used with JSON/CSV loading).

from_skopt

from_skopt(dimensions)

Load search space from skopt dimensions.

to_dict

to_dict() -> List[Dict[str, Any]]

Convert search space to a list of dictionaries.

to_skopt

to_skopt() -> List[Union[Real, Integer, Categorical]]

Get skopt dimensions for scikit-learn.

to_ax_space

to_ax_space() -> Dict[str, Dict[str, Any]]

Convert to Ax parameter format (controllable variables only).

to_botorch_bounds

to_botorch_bounds() -> Dict[str, np.ndarray]

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

get_variable_names() -> List[str]

All variable names, controllable first, then observed (the model's column order).

get_controllable_variable_names

get_controllable_variable_names() -> List[str]

Names of controllable variables in registration order.

get_observed_variable_names

get_observed_variable_names() -> List[str]

Names of observed variables (including legacy context) in registration order.

get_tunable_variable_names

get_tunable_variable_names() -> List[str]

Alias of :meth:get_controllable_variable_names.

get_context_variable_names

get_context_variable_names() -> List[str]

Alias of :meth:get_observed_variable_names.

get_variables_by_changes

get_variables_by_changes(level: str) -> List[Dict[str, Any]]

Controllable variable dicts whose change difficulty is level.

get_categorical_variables

get_categorical_variables() -> List[str]

Get list of categorical variable names.

check_categorical_values

check_categorical_values(data: Union[Dict[str, Any], DataFrame]) -> None

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.

Example
space = SearchSpace()
space.add_variable("c", "categorical", values=["a", "b"])
space.check_categorical_values({"c": "a"})   # passes
space.check_categorical_values({"c": "B"})   # raises ValueError

get_integer_variables

get_integer_variables() -> List[str]

Get list of integer variable names.

get_discrete_variables

get_discrete_variables() -> List[str]

Get list of discrete variable names.

add_derived_variable

add_derived_variable(name: str, func, input_cols: List[str], description: str = '') -> None

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 func(row: dict) -> float. Pass None when restoring a stub from a saved session.

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

register_derived_variable(name: str, func) -> None

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 func(row: dict) -> float.

required

Raises:

Type Description
ValueError

If no derived variable with the given name exists.

add_derived_variable_stub

add_derived_variable_stub(name: str, input_cols: List[str], description: str = '') -> None

Restore a derived variable stub from session JSON (func=None).

has_derived_variables

has_derived_variables() -> bool

Return True if any derived variables are registered.

get_derived_variable_names

get_derived_variable_names() -> List[str]

Return list of derived variable names (in registration order).

derived_variables_to_dict

derived_variables_to_dict() -> List[Dict[str, Any]]

Return serializable metadata for all derived variables (no func).

save_to_json

save_to_json(filepath: str)

Save search space to a JSON file.

load_from_json

load_from_json(filepath: str)

Load search space from a JSON file.

from_json classmethod

from_json(filepath: str)

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 constraint_N when omitted. Names identify a constraint for removal, so an explicit name that duplicates an existing one raises ValueError.

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 None or string value arriving from a JSON file must fail through the documented channel.

restore_constraints

restore_constraints(raw: Any) -> None

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 constraints value. None and a missing key both mean "no constraints", which is what a file written before the key existed carries.

required

Raises:

Type Description
ValueError

raw is not a list, an entry is not a dict, or an entry fails :meth:_validate_constraint_shape. The message names the entry's index and, when it has one, its name -- without which "your session will not load" is unactionable.

get_constraints

get_constraints() -> List[Dict]

Return list of constraint dicts.

filter_feasible

filter_feasible(points, rtol: float = 0.001, atol: float = 1e-06) -> np.ndarray

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 atol for how the two combine.

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

is_feasible(point, rtol: float = 0.001, atol: float = 1e-06) -> bool

Whether a single point (dict or 1-row DataFrame) is feasible.

to_botorch_constraints

to_botorch_constraints(feature_names: List[str]) -> Tuple[Optional[List], Optional[List]]

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

role_of(var: Dict[str, Any]) -> str

'observed' for an observed (or legacy context) variable, else 'controllable'.

kind_of

kind_of(var: Dict[str, Any]) -> str

The value kind: real, integer, discrete or categorical.

changes_of

changes_of(var: Dict[str, Any]) -> Optional[str]

Change difficulty of a controllable variable; None for an observed one.