Skip to content

Design spaces

banditry.variable_domains.design_space.DesignSpace()

Search-space definition for a contextual-bandit / optimisation problem.

A design space is built from a list of spec dicts via :meth:parse, one dict per parameter. The built-in parameter types (see :attr:parameter_types) accept:

  • {"name": ..., "type": "num", "lb": float, "ub": float} -- continuous parameter.
  • {"name": ..., "type": "int", "lb": int, "ub": int} -- integer parameter (inclusive bounds).
  • {"name": ..., "type": "bool"} -- boolean parameter.
  • {"name": ..., "type": "cat", "categories": [...]} -- categorical parameter.

Custom types can be added with :meth:register_parameter_type.

Note on ordering: :attr:para_names lists the numeric-ish parameters (num/int/bool) first, followed by the categorical ones, regardless of their order in the spec list.

Example

space = DesignSpace.parse( ... [ ... {"name": "lr", "type": "num", "lb": 1e-4, "ub": 1e-1}, ... {"name": "num_layers", "type": "int", "lb": 1, "ub": 8}, ... {"name": "use_bias", "type": "bool"}, ... {"name": "activation", "type": "cat", "categories": ["relu", "tanh", "gelu"]}, ... ] ... ) df = space.sample(5) x_num, x_cat = space.transform(df) # FloatTensor (5, 3), LongTensor (5, 1) space.inverse_transform(x_num, x_cat).columns.tolist() ['lr', 'num_layers', 'use_bias', 'activation']

Source code in src/banditry/variable_domains/design_space.py
49
50
51
52
53
def __init__(self):
    self.paras = {}
    self.para_names = []
    self.numeric_names = []
    self.enum_names = []

num_paras property

Total number of parameters in the design space.

num_numeric property

Number of numeric-ish parameters (num/int/bool).

num_categorical property

Number of categorical parameters.

opt_lb property

Per-parameter lower bounds in the transformed/optimiser domain (numeric first, then categorical).

opt_ub property

Per-parameter upper bounds in the transformed/optimiser domain (numeric first, then categorical).

parse(rec) classmethod

Build a :class:DesignSpace from a list of parameter spec dicts.

Parameters:

Name Type Description Default
rec list[dict]

List of spec dicts, one per parameter. Each dict must have a unique "name" and a "type" registered in :attr:parameter_types (built-ins: "num", "int", "bool", "cat"); the remaining keys are type-specific (e.g. "lb"/"ub" or "categories"). See the class docstring for the full format.

required

Returns:

Type Description
DesignSpace

A new :class:DesignSpace whose para_names are ordered numeric-first,

DesignSpace

then categorical.

Source code in src/banditry/variable_domains/design_space.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
@classmethod
def parse(cls, rec: list[dict]) -> "DesignSpace":
    """Build a :class:`DesignSpace` from a list of parameter spec dicts.

    Args:
        rec: List of spec dicts, one per parameter. Each dict must have a unique
            ``"name"`` and a ``"type"`` registered in :attr:`parameter_types`
            (built-ins: ``"num"``, ``"int"``, ``"bool"``, ``"cat"``); the remaining
            keys are type-specific (e.g. ``"lb"``/``"ub"`` or ``"categories"``).
            See the class docstring for the full format.

    Returns:
        A new :class:`DesignSpace` whose ``para_names`` are ordered numeric-first,
        then categorical.
    """
    object = cls()
    object.para_config = rec
    object.paras = {}
    object.para_names = []
    for item in rec:
        assert item["type"] in DesignSpace.parameter_types
        param = DesignSpace.parameter_types[item["type"]](item)
        object.paras[param.name] = param
        if param.is_categorical:
            object.enum_names.append(param.name)
        else:
            object.numeric_names.append(param.name)
    object.para_names = object.numeric_names + object.enum_names
    assert len(object.para_names) == len(set(object.para_names))
    return object

register_parameter_type(type_name, para_class) staticmethod

Register a custom parameter type.

After registration, type_name can be used as the "type" value in the spec dicts passed to :meth:parse.

Parameters:

Name Type Description Default
type_name str

Identifier to use as the "type" value in spec dicts.

required
para_class type[Parameter]

A :class:Parameter subclass; it is constructed with the spec dict of each parameter declared with this type.

required
Source code in src/banditry/variable_domains/design_space.py
101
102
103
104
105
106
107
108
109
110
111
112
113
@staticmethod
def register_parameter_type(type_name: str, para_class: type[Parameter]):
    """Register a custom parameter type.

    After registration, ``type_name`` can be used as the ``"type"`` value in the
    spec dicts passed to :meth:`parse`.

    Args:
        type_name: Identifier to use as the ``"type"`` value in spec dicts.
        para_class: A :class:`Parameter` subclass; it is constructed with the
            spec dict of each parameter declared with this type.
    """
    DesignSpace.parameter_types[type_name] = para_class

transform(data)

Map a DataFrame of raw parameter values into the model/optimiser domain.

Parameters:

Name Type Description Default
data DataFrame

DataFrame with one column per parameter holding raw values, as produced by :meth:sample or supplied by the user.

required

Returns:

Type Description
Tensor

Tuple (x_numerical, x_categorical): x_numerical is a FloatTensor

Tensor

of shape (n, num_numeric) with the transformed num/int/bool

tuple[Tensor, Tensor]

columns, and x_categorical is a LongTensor of shape

tuple[Tensor, Tensor]

(n, num_categorical) holding category indices.

Source code in src/banditry/variable_domains/design_space.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def transform(self, data: pd.DataFrame) -> tuple[Tensor, Tensor]:
    """Map a DataFrame of raw parameter values into the model/optimiser domain.

    Args:
        data: DataFrame with one column per parameter holding raw values, as
            produced by :meth:`sample` or supplied by the user.

    Returns:
        Tuple ``(x_numerical, x_categorical)``: ``x_numerical`` is a ``FloatTensor``
        of shape ``(n, num_numeric)`` with the transformed ``num``/``int``/``bool``
        columns, and ``x_categorical`` is a ``LongTensor`` of shape
        ``(n, num_categorical)`` holding category indices.
    """
    x_numerical = data[self.numeric_names].values.astype(float).copy()
    x_categorical = data[self.enum_names].values.copy()
    for i, name in enumerate(self.numeric_names):
        x_numerical[:, i] = self.paras[name].transform(x_numerical[:, i])
    for i, name in enumerate(self.enum_names):
        x_categorical[:, i] = self.paras[name].transform(x_categorical[:, i])
    return torch.FloatTensor(x_numerical), torch.LongTensor(x_categorical.astype(int))

inverse_transform(x_numerical, x_categorical)

Map transformed tensors back to a DataFrame of raw parameter values.

Parameters:

Name Type Description Default
x_numerical Tensor

Tensor of shape (n, num_numeric) in the transformed domain.

required
x_categorical Tensor

Tensor of shape (n, num_categorical) holding category indices.

required

Returns:

Type Description
DataFrame

DataFrame with one column per parameter holding raw values (floats, ints,

DataFrame

bools and the original category labels).

Source code in src/banditry/variable_domains/design_space.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def inverse_transform(self, x_numerical: Tensor, x_categorical: Tensor) -> pd.DataFrame:
    """Map transformed tensors back to a DataFrame of raw parameter values.

    Args:
        x_numerical: Tensor of shape ``(n, num_numeric)`` in the transformed domain.
        x_categorical: Tensor of shape ``(n, num_categorical)`` holding category indices.

    Returns:
        DataFrame with one column per parameter holding raw values (floats, ints,
        bools and the original category labels).
    """
    with torch.no_grad():
        inv_dict = {}
        for i, name in enumerate(self.numeric_names):
            inv_dict[name] = self.paras[name].inverse_transform(x_numerical.detach().double().numpy()[:, i])
        for i, name in enumerate(self.enum_names):
            inv_dict[name] = self.paras[name].inverse_transform(x_categorical.detach().numpy()[:, i])
        return pd.DataFrame(inv_dict)

to_pymoo_vars()

Express the design space as a dict of pymoo decision variables.

Returns:

Type Description

Dict mapping each parameter name to a pymoo variable: Real for

continuous parameters, Integer for numeric parameters that stay

discrete after transform (int/bool), and Choice over the

category indices for categorical parameters.

Raises:

Type Description
ValueError

If a parameter is neither numeric nor categorical.

Source code in src/banditry/variable_domains/design_space.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def to_pymoo_vars(self):
    """Express the design space as a dict of pymoo decision variables.

    Returns:
        Dict mapping each parameter name to a pymoo variable: ``Real`` for
        continuous parameters, ``Integer`` for numeric parameters that stay
        discrete after transform (``int``/``bool``), and ``Choice`` over the
        category indices for categorical parameters.

    Raises:
        ValueError: If a parameter is neither numeric nor categorical.
    """
    vars = {}
    for i, p_name in enumerate(self.para_names):
        p = self.paras[p_name]
        lb = self.opt_lb[i].item()
        ub = self.opt_ub[i].item()
        if p.is_numeric:
            if not p.is_discrete_after_transform:
                vars[p_name] = Real(bounds=(lb, ub))
            else:
                # WARNING: Only for GenAlg type methods, others may require non-linear transformations
                vars[p_name] = Integer(bounds=(lb, ub))
        elif p.is_categorical:
            vars[p_name] = Choice(options=list(range(int(lb), int(ub + 1))))
        else:
            raise ValueError("Unknown parameter type: {p_name}")
    return vars

sample(num_samples=1)

Draw uniform random samples of the raw parameter values.

Parameters:

Name Type Description Default
num_samples int

Number of rows to draw.

1

Returns:

Type Description
DataFrame

DataFrame of shape (num_samples, num_paras) with columns ordered as

DataFrame

attr:para_names and raw (untransformed) values.

Source code in src/banditry/variable_domains/design_space.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def sample(self, num_samples: int = 1) -> pd.DataFrame:
    """Draw uniform random samples of the raw parameter values.

    Args:
        num_samples: Number of rows to draw.

    Returns:
        DataFrame of shape ``(num_samples, num_paras)`` with columns ordered as
        :attr:`para_names` and raw (untransformed) values.
    """
    df = pd.DataFrame(columns=self.para_names)
    for c in df.columns:
        df[c] = self.paras[c].sample(num_samples)
    return df

banditry.variable_domains.parameter_types

Parameter(param_dict)

Bases: ABC

Abstract base class for a single design-space parameter.

A parameter converts values between two representations:

  • the raw domain, as produced by :meth:sample and seen by the user (floats, ints, bools, category labels), and
  • the transformed / optimiser domain, a numeric encoding bounded by :attr:opt_lb and :attr:opt_ub that surrogate models and acquisition optimisers operate on.

Subclasses implement :meth:sample, :meth:transform (raw to transformed) and :meth:inverse_transform (transformed back to raw), plus the is_numeric, is_discrete and is_discrete_after_transform flags that determine how the optimiser treats the parameter. is_categorical is simply not is_numeric.

Parameters:

Name Type Description Default
param_dict

Spec dict for this parameter; must contain at least "name".

required
Source code in src/banditry/variable_domains/parameter_types.py
27
28
29
30
def __init__(self, param_dict):
    self.param_dict = param_dict
    self.name = param_dict["name"]
    pass

NumericParameter(param_dict)

Bases: Parameter

Continuous parameter on [lb, ub] (spec type "num").

Reads the spec keys "name", "lb" and "ub". The transform is the identity, so the optimiser domain equals the raw domain with bounds opt_lb = lb and opt_ub = ub. Sampling is uniform on [lb, ub).

Source code in src/banditry/variable_domains/parameter_types.py
82
83
84
85
def __init__(self, param_dict):
    super().__init__(param_dict)
    self.lb = param_dict["lb"]
    self.ub = param_dict["ub"]

CategoricalParameter(param)

Bases: Parameter

Categorical parameter over a fixed set of choices (spec type "cat").

Reads the spec keys "name" and "categories". transform maps each category label to its (float) index in categories; inverse_transform rounds to the nearest index and returns the corresponding label. Optimiser bounds are [0, len(categories) - 1] and sampling is uniform over the categories.

Source code in src/banditry/variable_domains/parameter_types.py
128
129
130
131
132
133
134
135
136
def __init__(self, param):
    super().__init__(param)
    self.categories = list(param["categories"])
    try:
        self._categories_dict = {k: v for v, k in enumerate(self.categories)}
    except TypeError:
        self._categories_dict = None
    self.lb = 0
    self.ub = len(self.categories) - 1

BoolParameter(param)

Bases: Parameter

Boolean parameter (spec type "bool").

Reads only the spec key "name". transform casts to float (0.0/1.0); inverse_transform thresholds at 0.5 (x > 0.5), so any optimiser value in [0, 1] maps back to a bool. Sampling is a fair coin flip.

Source code in src/banditry/variable_domains/parameter_types.py
185
186
187
188
def __init__(self, param):
    super().__init__(param)
    self.lb = 0
    self.ub = 1

IntParameter(param_dict)

Bases: Parameter

Integer parameter on the inclusive range [lb, ub] (spec type "int").

Reads the spec keys "name", "lb" and "ub" (rounded to the nearest integer). transform casts to float; inverse_transform rounds back to the nearest integer. Sampling is uniform over the integers in [lb, ub].

Source code in src/banditry/variable_domains/parameter_types.py
229
230
231
232
def __init__(self, param_dict):
    super().__init__(param_dict)
    self.lb = round(param_dict["lb"])
    self.ub = round(param_dict["ub"])