Problem Definition with Complex Search Space#

In quick examples, we have shown how to define independent variables in the search space. However, real-world search spaces are often complex, which may contain in-space conditions, forbidden clauses, etc. In this tutorial, we will show how OpenBox supports complex search spaces.

Hierarchical Conditions#

The search space in OpenBox is currently built on the package ConfigSpace. While ConfigSpace supports hierarchical conditions, we can directly use the advanced APIs provided in ConfigSpace.

In the following example, we provide an example of building a hierarchical search space using Conditions:

from openbox import space as sp
from ConfigSpace import EqualsCondition, InCondition

space = sp.Space()
x1 = sp.Categorical("x1", choices=["c1", "c2", "c3", "c4"])
x2 = sp.Real("x2", -5, 10, default_value=0)
x3 = sp.Real("x3", 0, 15, default_value=0)

equal_condition = EqualsCondition(x2, x1, "c1")  # x2 is active when x1 = c1
in_condition = InCondition(x3, x1, ["c2", "c3"])  # x3 is active when x1 = c2 or x1 = c3

space.add_variables([x1, x2, x3])
space.add_conditions([equal_condition, in_condition])

print(space.sample_configuration(5))

The example output can be as follows,

[Configuration(values={
  'x1': 'c4',
})
, Configuration(values={
  'x1': 'c1',
  'x2': -4.246561408224157,
})
, Configuration(values={
  'x1': 'c3',
  'x3': 1.7213163807467695,
})
, Configuration(values={
  'x1': 'c3',
  'x3': 13.8469881579991,
})
, Configuration(values={
  'x1': 'c2',
  'x3': 2.9833423891692763,
})
]

In this example, the variable x2 is active when the value of x1 is c1. The variable x3 is active when the value of x1 is c2 or c3. When the value of x1 is c4, neither x2 nor x3 is active. The value of inactive variables are set to np.nan by default. During optimization, invalid variable combinations will not be sampled, so there will be no evaluation time spent for invalid configurations.

By utilizing Conditions, users can build complex search space with hierarchical structure. For more details about Conditions, please refer to the ConfigSpace documentation for more details.

In-space variable Constraints#

To support constraints between variables (e.g., a<b or a+b<10), we recommend adding a sampling condition as shown in the following example,

from openbox import space as sp

def sample_condition(config):
    # require x1 <= x2 and x1 * x2 < 100
    if config['x1'] > config['x2']:
        return False
    if config['x1'] * config['x2'] >= 100:
        return False
    return True
    # return config['x1'] <= config['x2'] and config['x1'] * config['x2'] < 100

cs = sp.ConditionedSpace()
cs.add_variables([...])
cs.set_sample_condition(sample_condition)  # set the sample condition after all variables are added

The API set_sample_condition requires a function that takes a configuration as the input and output a boolean value, which indicates whether the configuration is valid. Only configurations that meet the sample condition (with a return value of True) are sampled during optimization.

Note that, the in-space variable constraints are constraints that can be checked directly without running the objective function, which are different from the black-box constraints in constrained problems.