Custom Planners

You can integrate your own optimization algorithm within Olympus. A simple example where we create a Planner that implements a random sampler can be found among the examples.

In brief, one can implement custom algorithms by inheriting from the CustomPlanner class:

from olympus.planners import CustomPlanner

Here, we will discuss in more detail the methods implemented in this class to allow the user to customize it as needed.

CustomPlanner class already comes with __init__, _set_param_space, _tell methods. This is important because all Planner instances in Olympus are expected to have these three methods, as well as an _ask method. Thus, at a minimum a custom planner class needs to implement the _ask method, while expanding upon the default __init__, _set_param_space, _tell methods is optional. Here below we provide a quick summary of what these four methods should do.

__init__

This method works as in all Python classes, and for any Planner in Olympus, the minimal __init__ should look like this:

def __init__(self, goal='minimize'):
    AbstractPlanner.__init__(**locals())

Where goal is a required “abstract attribute” and the parent AbstractPlanner class should be initialised with all variables in the local namespace. This is the default implemented in CustomPlanner. A customized __init__ method could contain additional arguments specific to your algorithms. For instance, it could take the random_seed argument if the algorithm is stochastic.

_set_param_space

This method defines the domain for the optimization. Only points within this search space should be queried by the algorithm. This is what is implemented by default in CustomPlanner:

def _set_param_space(self, param_space):
    self._param_space = []
    for param in param_space:
        if param.type == 'continuous':
            param_dict = {'name': param.name, 'type': param.type, 'domain': (param.low, param.high)}
            self._param_space.append(param_dict)

Basically, it creates a _param_space attribute that is a list of dictionaries, with the name of the parameters, their type (only ‘continuous’ is currently supported) and their bounds. You can then use this attribute to make sure your algorithm samples points within the defined optimisation domain.

_tell

This method provides the Planner instance with information about the history of the optimization, which may be used to select the next query point. This is what is implemented by default in CustomPlanner:

def _tell(self, observations):
    self._params = observations.get_params(as_array=True)
    self._values = observations.get_values(as_array=True, opposite=self.flip_measurements)

It defined _params and _values attributes that contain all previous queried parameter locations and their merit, respectively. If the Planner was initialised with goal='minimize', then self.flip_measurements will be False. If it was initialised with goal='maximize', then self.flip_measurements will be True, which means that the _values will contain the merits multiplied by -1. This is because Planner instances are expected to always aim at minimizing the objective function.

_ask

This is the only method that has no implementation in CustomPlanner and always needs to be implemented by the user. What this method is expected to do is to decide which next set of parameters to query, and return this as a ParameterVector instance. This is an example in which we iterate over all dimensions of parameter space to sample the space at random:

def _ask(self):
    new_params = []  # list with the parameter values for the next query location
    # go through all dimensions of parameter space
    for param in self._param_space:
        # sample uniformly at random within the bounds of the specific dimension
        new_param = np.random.uniform(low=param['domain'][0], high=param['domain'][1])
        # append to the list with the parameter values
        new_params.append(new_param)

    # return new_params as a ParameterVector object
    return ParameterVector(array=new_params, param_space=self.param_space)

In this case, the next sample does not depend on the history of the search, so we have not used _params and _values, but if you are implementing, e.g., a Bayesian optimization algorithm, you can used these attributes too.