Source code for orion.algo.space

# -*- coding: utf-8 -*-
# pylint:disable=too-many-lines
"""
Search space of optimization problems
=====================================

Classes for representing the search space of an optimization problem.

There are 3 classes representing possible parameter types. All of them subclass
the base class `Dimension`:

    * `Real`
    * `Integer`
    * `Categorical`

These are instantiated to declare a problem's parameter space. Oríon registers
them in a ordered dictionary, `Space`, which describes how the parameters should
be in order for `orion.algo.base.AbstractAlgorithm` implementations to
communicate with `orion.core`.

Parameter values recorded in `orion.core.worker.trial.Trial` objects must be
and are in concordance with `orion.algo.space` objects. These objects will be
defined by `orion.core` using the user script's configuration file.

Prior distributions, contained in `Dimension` classes, are based on
:scipy.stats:`distributions` and should be configured as noted in the
scipy documentation for each specific implentation of a random variable type,
unless noted otherwise!

"""
import copy
import logging
import numbers

import numpy
from scipy.stats import distributions

from orion.core.utils import float_to_digits_list, format_trials
from orion.core.utils.flatten import flatten

logger = logging.getLogger(__name__)


[docs]def check_random_state(seed): """Return numpy global rng or RandomState if seed is specified""" if seed is None or seed is numpy.random: rng = ( numpy.random.mtrand._rand ) # pylint:disable=protected-access,c-extension-no-member elif isinstance(seed, numpy.random.RandomState): rng = seed else: try: rng = numpy.random.RandomState(seed) except Exception as e: raise ValueError( "%r cannot be used to seed a numpy.random.RandomState" " instance" % seed ) from e return rng
# helper class to be able to print [1, ..., 4] instead of [1, '...', 4] class _Ellipsis: # pylint:disable=too-few-public-methods def __repr__(self): return "..."
[docs]class Dimension: """Base class for search space dimensions. Attributes ---------- name : str Unique identifier for this `Dimension`. type : str Identifier for the type of parameters this `Dimension` is representing. it can be 'real', 'integer', or 'categorical' (name of a subclass). prior : `scipy.stats.distributions.rv_generic` A distribution over the original dimension. shape : tuple Defines how many dimensions are packed in this `Dimension`. Describes the shape of the corresponding tensor. """ NO_DEFAULT_VALUE = None def __init__(self, name, prior, *args, **kwargs): """Init code which is common for `Dimension` subclasses. Parameters ---------- name : str Unique identifier associated with this `Dimension`, e.g. 'learning_rate'. prior : str | `scipy.stats.distributions.rv_generic` Corresponds to a name of an instance or an instance itself of `scipy.stats.distributions.rv_generic`. Basically, the name of the distribution one wants to use as a :attr:`prior`. args : list kwargs : dict Shape parameter(s) for the `prior` distribution. Should include all the non-optional arguments. It may include ``loc``, ``scale``, ``shape``. .. seealso:: `scipy.stats.distributions` for possible values of `prior` and their arguments. """ self._name = None self.name = name if isinstance(prior, str): self._prior_name = prior self.prior = getattr(distributions, prior) elif prior is None: self._prior_name = "None" self.prior = prior else: self._prior_name = prior.name self.prior = prior self._args = args self._kwargs = kwargs self._default_value = kwargs.pop("default_value", self.NO_DEFAULT_VALUE) self._shape = kwargs.pop("shape", None) self.validate()
[docs] def validate(self): """Validate dimension arguments""" if "random_state" in self._kwargs or "seed" in self._kwargs: raise ValueError( "random_state/seed cannot be set in a " "parameter's definition! Set seed globally!" ) if "discrete" in self._kwargs: raise ValueError( "Do not use kwarg 'discrete' on `Dimension`, " "use pure `_Discrete` class instead!" ) if "size" in self._kwargs: raise ValueError("Use 'shape' keyword only instead of 'size'.") if ( self.default_value is not self.NO_DEFAULT_VALUE and self.default_value not in self ): raise ValueError( "{} is not a valid value for this Dimension. " "Can't set default value.".format(self.default_value) )
def _get_hashable_members(self): return ( self.name, self.shape, self.type, tuple(self._args), tuple(self._kwargs.items()), self.default_value, self._prior_name, ) # pylint:disable=protected-access def __eq__(self, other): """Return True if other is the same dimension as self""" if not isinstance(other, Dimension): return False return self._get_hashable_members() == other._get_hashable_members() def __hash__(self): """Return the hash of the hashable members""" return hash(self._get_hashable_members())
[docs] def sample(self, n_samples=1, seed=None): """Draw random samples from `prior`. Parameters ---------- n_samples : int, optional The number of samples to be drawn. Default is 1 sample. seed : None | int | ``numpy.random.RandomState`` instance, optional This parameter defines the RandomState object to use for drawing random variates. If None (or np.random), the **global** np.random state is used. If integer, it is used to seed a RandomState instance **just for the call of this function**. Default is None. Set random state to something other than None for reproducible results. .. warning:: Setting `seed` with an integer will cause the same ndarray to be sampled if ``n_samples > 0``. Set `seed` with a ``numpy.random.RandomState`` to carry on the changes in random state across many samples. """ samples = [ self.prior.rvs( *self._args, size=self.shape, random_state=seed, **self._kwargs ) for _ in range(n_samples) ] return samples
[docs] def cast(self, point): """Cast a point to dimension's type If casted point will stay a list or a numpy array depending on the given point's type. """ raise NotImplementedError
[docs] def interval(self, alpha=1.0): """Return a tuple containing lower and upper bound for parameters. If parameters are drawn from an 'open' supported random variable, then it will be attempted to calculate the interval from which a variable is `alpha`-likely to be drawn from. """ return self.prior.interval(alpha, *self._args, **self._kwargs)
def __contains__(self, point): """Check if constraints hold for this `point` of `Dimension`. :param point: a parameter corresponding to this `Dimension`. :type point: numeric or array-like .. note:: Default `Dimension` does not have any extra constraints. It just checks whether point lies inside the support and the shape. """ raise NotImplementedError def __repr__(self): """Represent the object as a string.""" return "{0}(name={1}, prior={{{2}: {3}, {4}}}, shape={5}, default value={6})".format( self.__class__.__name__, self.name, self._prior_name, self._args, self._kwargs, self.shape, self._default_value, )
[docs] def get_prior_string(self): """Build the string corresponding to current prior""" args = copy.deepcopy(list(self._args[:])) if self._prior_name == "uniform" and len(args) == 2: args[1] = args[0] + args[1] args[0] = args[0] args = list(map(str, args)) for k, v in self._kwargs.items(): if isinstance(v, str): args += ["{}='{}'".format(k, v)] else: args += ["{}={}".format(k, v)] if self._shape is not None: args += ["shape={}".format(self._shape)] if self.default_value is not self.NO_DEFAULT_VALUE: args += ["default_value={}".format(repr(self.default_value))] prior_name = self._prior_name if prior_name == "reciprocal": prior_name = "loguniform" if prior_name == "norm": prior_name = "normal" return "{prior_name}({args})".format( prior_name=prior_name, args=", ".join(args) )
[docs] def get_string(self): """Build the string corresponding to current dimension""" return "{name}~{prior}".format(name=self.name, prior=self.get_prior_string())
@property def name(self): """See `Dimension` attributes.""" return self._name @name.setter def name(self, value): if isinstance(value, str) or value is None: self._name = value else: raise TypeError( "Dimension's name must be either string or None. " "Provided: {}, of type: {}".format(value, type(value)) ) @property def default_value(self): """Return the default value for this dimensions""" return self._default_value @property def type(self): """See `Dimension` attributes.""" return self.__class__.__name__.lower() @property def prior_name(self): """Return the name of the prior""" return self._prior_name @property def shape(self): """Return the shape of dimension.""" # Default shape `None` corresponds to 0-dim (scalar) or shape == (). # Read about ``size`` argument in # `scipy.stats._distn_infrastructure.rv_generic._argcheck_rvs` if self.prior is None: return None _, _, _, size = self.prior._parse_args_rvs( *self._args, # pylint:disable=protected-access size=self._shape, **self._kwargs, ) return size # pylint:disable=no-self-use @property def cardinality(self): """Return the number of all the possible points from `Dimension`. The default value is ``numpy.inf``. """ return numpy.inf
def _is_numeric_array(point): """Test whether a point is numerical object or an array containing only numerical objects""" def _is_numeric(item): return isinstance(item, (numbers.Number, numpy.ndarray)) try: return numpy.all(numpy.vectorize(_is_numeric)(point)) except TypeError: return _is_numeric(point) return False
[docs]class Real(Dimension): """Search space dimension that can take on any real value. Parameters ---------- name: str prior: str See Parameters of `Dimension.__init__()`. args: list kwargs: dict See Parameters of `Dimension.__init__()` for general. Notes ----- Real kwargs (extra) low: float Lower bound (inclusive), optional; default ``-numpy.inf``. high: float: Upper bound (inclusive), optional; default ``numpy.inf``. The upper bound must be inclusive because of rounding errors during optimization which may cause values to round exactly to the upper bound. precision: int Precision, optional; default ``4``. shape: tuple Defines how many dimensions are packed in this `Dimension`. Describes the shape of the corresponding tensor. """ def __init__(self, name, prior, *args, **kwargs): self._low = kwargs.pop("low", -numpy.inf) self._high = kwargs.pop("high", numpy.inf) if self._high <= self._low: raise ValueError( "Lower bound {} has to be less than upper bound {}".format( self._low, self._high ) ) precision = kwargs.pop("precision", 4) if (isinstance(precision, int) and precision > 0) or precision is None: self.precision = precision else: raise TypeError( "Precision should be a non-negative int or None, " "instead was {} of type {}.".format(precision, type(precision)) ) super(Real, self).__init__(name, prior, *args, **kwargs) def __contains__(self, point): """Check if constraints hold for this `point` of `Dimension`. :param point: a parameter corresponding to this `Dimension`. :type point: numeric or array-like .. note:: Default `Dimension` does not have any extra constraints. It just checks whether point lies inside the support and the shape. """ if not _is_numeric_array(point): return False low, high = self.interval() point_ = numpy.asarray(point) if point_.shape != self.shape: return False return numpy.all(point_ >= low) and numpy.all(point_ <= high)
[docs] def get_prior_string(self): """Build the string corresponding to current prior""" prior_string = super(Real, self).get_prior_string() if self.precision != 4: return prior_string[:-1] + f", precision={self.precision})" return prior_string
[docs] def interval(self, alpha=1.0): """Return a tuple containing lower and upper bound for parameters. If parameters are drawn from an 'open' supported random variable, then it will be attempted to calculate the interval from which a variable is `alpha`-likely to be drawn from. .. note:: Both lower and upper bounds are inclusive. """ prior_low, prior_high = super(Real, self).interval(alpha) return (max(prior_low, self._low), min(prior_high, self._high))
[docs] def sample(self, n_samples=1, seed=None): """Draw random samples from `prior`. .. seealso:: `Dimension.sample` """ samples = [] for _ in range(n_samples): for _ in range(4): sample = super(Real, self).sample(1, seed) if sample[0] not in self: nice = False continue nice = True samples.extend(sample) break if not nice: raise ValueError( "Improbable bounds: (low={0}, high={1}). " "Please make interval larger.".format(self._low, self._high) ) return samples
# pylint:disable=no-self-use
[docs] def cast(self, point): """Cast a point to float If casted point will stay a list or a numpy array depending on the given point's type. """ casted_point = numpy.asarray(point).astype(float) if not isinstance(point, numpy.ndarray): return casted_point.tolist() return casted_point
[docs] @staticmethod def get_cardinality(shape, interval, precision, prior_name): """Return the number of all the possible points based and shape and interval""" if precision is None or prior_name not in ["loguniform", "reciprocal"]: return numpy.inf # If loguniform, compute every possible combinations based on precision # for each orders of magnitude. def format_number(number): """Turn number into an array of digits, the size of the precision""" formated_number = numpy.zeros(precision) digits_list = float_to_digits_list(number) lenght = min(len(digits_list), precision) formated_number[:lenght] = digits_list[:lenght] return formated_number min_number = format_number(interval[0]) max_number = format_number(interval[1]) # Compute the number of orders of magnitude spanned by lower and upper bounds # (if lower and upper bounds on same order of magnitude, span is equal to 1) lower_order = numpy.floor(numpy.log10(numpy.abs(interval[0]))) upper_order = numpy.floor(numpy.log10(numpy.abs(interval[1]))) order_span = upper_order - lower_order + 1 # Total number of possibilities for an order of magnitude full_cardinality = 9 * 10 ** (precision - 1) def num_below(number): return ( numpy.clip(number, a_min=0, a_max=9) * 10 ** numpy.arange(precision - 1, -1, -1) ).sum() # Number of values out of lower bound on lowest order of magnitude cardinality_below = num_below(min_number) # Number of values out of upper bound on highest order of magnitude. # Remove 1 to be inclusive. cardinality_above = full_cardinality - num_below(max_number) - 1 # Full cardinality on all orders of magnitude, minus those out of bounds. cardinality = ( full_cardinality * order_span - cardinality_below - cardinality_above ) return int(cardinality) ** int(numpy.prod(shape) if shape else 1)
@property def cardinality(self): """Return the number of all the possible points from Integer `Dimension`""" return Real.get_cardinality( self.shape, self.interval(), self.precision, self._prior_name )
class _Discrete(Dimension): def sample(self, n_samples=1, seed=None): """Draw random samples from `prior`. Discretizes with `numpy.floor` the results from `Dimension.sample`. .. seealso:: `Dimension.sample` .. seealso:: Discussion in https://github.com/epistimio/orion/issues/56 if you want to understand better how this `Integer` diamond inheritance works. """ samples = super(_Discrete, self).sample(n_samples, seed) # Making discrete by ourselves because scipy does not use **floor** return list(map(self.cast, samples)) def interval(self, alpha=1.0): """Return a tuple containing lower and upper bound for parameters. If parameters are drawn from an 'open' supported random variable, then it will be attempted to calculate the interval from which a variable is `alpha`-likely to be drawn from. Bounds are integers. .. note:: Both lower and upper bounds are inclusive. """ low, high = super(_Discrete, self).interval(alpha) try: int_low = int(numpy.floor(low)) except OverflowError: # infinity cannot be converted to Python int type int_low = -numpy.inf try: int_high = int(numpy.ceil(high)) except OverflowError: # infinity cannot be converted to Python int type int_high = numpy.inf return (int_low, int_high) def __contains__(self, point): raise NotImplementedError
[docs]class Integer(Real, _Discrete): """Search space dimension representing integer values. Parameters ---------- name: str prior: str See Parameters of `Dimension.__init__()`. args: list kwargs: dict See Parameters of `Dimension.__init__()` for general. Notes ----- Real kwargs (extra) low: float Lower bound (inclusive), optional; default ``-numpy.inf``. high: float: Upper bound (inclusive), optional; default ``numpy.inf``. precision: int Precision, optional; default ``4``. shape: tuple Defines how many dimensions are packed in this `Dimension`. Describes the shape of the corresponding tensor. """ def __contains__(self, point): """Check if constraints hold for this `point` of `Dimension`. :param point: a parameter corresponding to this `Dimension`. :type point: numeric or array-like `Integer` will check whether `point` contains only integers. """ if not _is_numeric_array(point): return False point_ = numpy.asarray(point) if not numpy.all(numpy.equal(numpy.mod(point_, 1), 0)): return False return super(Integer, self).__contains__(point)
[docs] def cast(self, point): """Cast a point to int If casted point will stay a list or a numpy array depending on the given point's type. """ casted_point = numpy.asarray(point).astype(float) # Rescale point to make high bound inclusive. low, high = self.interval() if not numpy.any(numpy.isinf([low, high])): high = high - low casted_point -= low casted_point = casted_point / high casted_point = casted_point * (high + (1 - 1e-10)) casted_point += low casted_point = numpy.floor(casted_point).astype(int) else: casted_point = numpy.floor(casted_point).astype(int) if not isinstance(point, numpy.ndarray): return casted_point.tolist() return casted_point
[docs] def get_prior_string(self): """Build the string corresponding to current prior""" prior_string = super(Integer, self).get_prior_string() return prior_string[:-1] + ", discrete=True)"
@property def prior_name(self): """Return the name of the prior""" return "int_{}".format(super(Integer, self).prior_name)
[docs] @staticmethod def get_cardinality(shape, interval): """Return the number of all the possible points based and shape and interval""" return int(interval[1] - interval[0] + 1) ** _get_shape_cardinality(shape)
@property def cardinality(self): """Return the number of all the possible points from Integer `Dimension`""" return Integer.get_cardinality(self.shape, self.interval())
def _get_shape_cardinality(shape): """Get the cardinality in a shape which can be int or tuple""" shape_cardinality = 1 if shape is None: return shape_cardinality if isinstance(shape, int): shape = (shape,) for cardinality in shape: shape_cardinality *= cardinality return shape_cardinality
[docs]class Categorical(Dimension): """Search space dimension that can take on categorical values. Parameters ---------- name : str See Parameters of `Dimension.__init__()`. categories : dict or other iterable A dictionary would associate categories to probabilities, else it assumes to be drawn uniformly from the iterable. kwargs : dict See Parameters of `Dimension.__init__()` for general. """ def __init__(self, name, categories, **kwargs): if isinstance(categories, dict): self.categories = tuple(categories.keys()) self._probs = tuple(categories.values()) else: self.categories = tuple(categories) self._probs = tuple(numpy.tile(1.0 / len(categories), len(categories))) # Just for compatibility; everything should be `Dimension` to let the # `Transformer` decorators be able to wrap smoothly anything. prior = distributions.rv_discrete( values=(list(range(len(self.categories))), self._probs) ) super(Categorical, self).__init__(name, prior, **kwargs)
[docs] @staticmethod def get_cardinality(shape, categories): """Return the number of all the possible points based and shape and categories""" return len(categories) ** _get_shape_cardinality(shape)
@property def cardinality(self): """Return the number of all the possible values from Categorical `Dimension`""" return Categorical.get_cardinality(self.shape, self.interval())
[docs] def sample(self, n_samples=1, seed=None): """Draw random samples from `prior`. .. seealso:: `Dimension.sample` """ rng = check_random_state(seed) cat_ndarray = numpy.array(self.categories, dtype=object) samples = [ rng.choice(cat_ndarray, p=self._probs, size=self._shape) for _ in range(n_samples) ] return samples
[docs] def interval(self, alpha=1.0): """Return a tuple of possible values that this categorical dimension can take.""" return self.categories
def __contains__(self, point): """Check if constraints hold for this `point` of `Dimension`. :param point: a parameter corresponding to this `Dimension`. :type point: numeric or array-like """ point_ = numpy.asarray(point, dtype=object) if point_.shape != self.shape: return False _check = numpy.vectorize(lambda x: x in self.categories) return numpy.all(_check(point_)) def __repr__(self): """Represent the object as a string.""" if len(self.categories) > 5: cats = self.categories[:2] + self.categories[-2:] probs = self._probs[:2] + self._probs[-2:] prior = list(zip(cats, probs)) prior.insert(2, _Ellipsis()) else: cats = self.categories probs = self._probs prior = list(zip(cats, probs)) prior = map( lambda x: "{0[0]}: {0[1]:.2f}".format(x) if not isinstance(x, _Ellipsis) else str(x), prior, ) prior = "{" + ", ".join(prior) + "}" return "Categorical(name={0}, prior={1}, shape={2}, default value={3})".format( self.name, prior, self.shape, self.default_value )
[docs] def get_prior_string(self): """Build the string corresponding to current prior""" args = list(map(str, self._args[:])) args += ["{}={}".format(k, v) for k, v in self._kwargs.items()] if self.default_value is not self.NO_DEFAULT_VALUE: args += ["default_value={}".format(self.default_value)] cats = [repr(c) for c in self.categories] if all(p == self._probs[0] for p in self._probs): prior = "[{}]".format(", ".join(cats)) else: probs = list(zip(cats, self._probs)) prior = "{" + ", ".join("{0}: {1:.2f}".format(c, p) for c, p in probs) + "}" args = [prior] if self._shape is not None: args += ["shape={}".format(self._shape)] if self.default_value is not self.NO_DEFAULT_VALUE: args += ["default_value={}".format(repr(self.default_value))] return "choices({args})".format(args=", ".join(args))
@property def get_prior(self): """Return the priors""" return self._probs @property def prior_name(self): """Return the name of the prior""" return "choices"
[docs] def cast(self, point): """Cast a point to some category Casted point will stay a list or a numpy array depending on the given point's type. Raises ------ ValueError If one of the category in `point` is not present in current Categorical Dimension. """ categorical_strings = {str(c): c for c in self.categories} def get_category(value): """Return category corresponding to a string else return singleton object""" if str(value) not in categorical_strings: raise ValueError("Invalid category: {}".format(value)) return categorical_strings[str(value)] point_ = numpy.asarray(point, dtype=object) cast = numpy.vectorize(get_category, otypes=[object]) casted_point = cast(point_) if not isinstance(point, numpy.ndarray): return casted_point.tolist() return casted_point
[docs]class Fidelity(Dimension): """Fidelity `Dimension` for representing multi-fidelity. Fidelity dimensions are not optimized by the algorithms. If it supports multi-fidelity, the algorithm will select a fidelity level for which it will sample hyper-parameter values to explore a low fidelity space. This class is used as a place-holder so that algorithms can discern fidelity dimensions from hyper-parameter dimensions. Parameters ---------- name : str Name of the dimension low: int Mininum of the fidelity interval. high: int Maximum of the fidelity interval. base: int Base logarithm of the fidelity dimension. Attributes ---------- name : str Name of the dimension default_value: int Maximum of the fidelity interval. """ # pylint:disable=super-init-not-called def __init__(self, name, low, high, base=2): if low <= 0: raise AttributeError("Minimum resources must be a positive number.") elif low > high: raise AttributeError( "Minimum resources must be smaller than maximum resources." ) if base < 1: raise AttributeError("Base should be greater than or equal to 1") self.name = name self.low = int(low) self.high = int(high) self.base = int(base) self.prior = None self._prior_name = "None" @property def default_value(self): """Return `high`""" return self.high
[docs] @staticmethod def get_cardinality(shape, interval): """Return cardinality of Fidelity dimension, leave it to 1 as Fidelity dimension does not contribute to cardinality in a fixed way now. """ return 1
@property def cardinality(self): """Return cardinality of Fidelity dimension, leave it to 1 as Fidelity dimension does not contribute to cardinality in a fixed way now. """ return Fidelity.get_cardinality(self.shape, self.interval())
[docs] def get_prior_string(self): """Build the string corresponding to current prior""" args = [str(self.low), str(self.high)] if self.base != 2: args += [f"base={self.base}"] return "fidelity({})".format(", ".join(args))
[docs] def validate(self): """Do not do anything.""" raise NotImplementedError
[docs] def sample(self, n_samples=1, seed=None): """Do not do anything.""" return [self.high for i in range(n_samples)]
[docs] def interval(self, alpha=1.0): """Do not do anything.""" return (self.low, self.high)
[docs] def cast(self, point=0): """Do not do anything.""" raise NotImplementedError
def __repr__(self): """Represent the object as a string.""" return "{0}(name={1}, low={2}, high={3}, base={4})".format( self.__class__.__name__, self.name, self.low, self.high, self.base ) def __contains__(self, value): """Check if constraints hold for this `point` of `Dimension`. :param point: a parameter corresponding to this `Dimension`. :type point: numeric or array-like """ return self.low <= value <= self.high
[docs]class Space(dict): """Represents the search space. It is a sorted dictionary which contains `Dimension` objects. The dimensions are sorted based on their names. """ contains = Dimension
[docs] def register(self, dimension): """Register a new dimension to `Space`.""" self[dimension.name] = dimension
[docs] def sample(self, n_samples=1, seed=None): """Draw random samples from this space. Parameters ---------- n_samples : int, optional The number of samples to be drawn. Default is 1 sample. seed : None | int | ``numpy.random.RandomState`` instance, optional This parameter defines the RandomState object to use for drawing random variates. If None (or np.random), the **global** np.random state is used. If integer, it is used to seed a RandomState instance **just for the call of this function**. Default is None. Set random state to something other than None for reproducible results. Returns ------- trials: list of `orion.core.worker.trial.Trial` Each element is a separate sample of this space, a trial containing values associated with the corresponding dimension. """ rng = check_random_state(seed) samples = [dim.sample(n_samples, rng) for dim in self.values()] return [format_trials.tuple_to_trial(point, self) for point in zip(*samples)]
[docs] def interval(self, alpha=1.0): """Return a list with the intervals for each contained dimension.""" res = list() for dim in self.values(): if dim.type == "categorical": res.append(dim.categories) else: res.append(dim.interval(alpha)) return res
def __getitem__(self, key): """Wrap __getitem__ to allow searching with position.""" if isinstance(key, str): return super(Space, self).__getitem__(key) values = list(self.values()) return values[key] def __setitem__(self, key, value): """Wrap __setitem__ to allow only ``Space.contains`` class, e.g. `Dimension`, values and string keys. """ if not isinstance(key, str): raise TypeError( "Keys registered to {} must be string types. " "Provided: {}".format(self.__class__.__name__, key) ) if not isinstance(value, self.contains): raise TypeError( "Values registered to {} must be {} types. " "Provided: {}".format( self.__class__.__name__, self.contains.__name__, value ) ) if key in self: raise ValueError( "There is already a Dimension registered with this name. " "Register it with another name. Provided: {}".format(key) ) super(Space, self).__setitem__(key, value) def __contains__(self, key_or_trial): """Check whether `trial` is within the bounds of the space. Or check if a name for a dimension is registered in this space. Parameters ---------- key_or_trial: str or `orion.core.worker.trial.Trial` If str, test if the string is a dimension part of the search space. If a Trial, test if trial's hyperparameters fit the current search space. """ if isinstance(key_or_trial, str): return super(Space, self).__contains__(key_or_trial) trial = key_or_trial flattened_params = flatten(trial.params) keys = set(flattened_params.keys()) for dim_name, dim in self.items(): if dim_name not in keys or flattened_params[dim_name] not in dim: return False keys.remove(dim_name) return len(keys) == 0 def __repr__(self): """Represent as a string the space and the dimensions it contains.""" dims = list(self.values()) return "Space([{}])".format(",\n ".join(map(str, dims)))
[docs] def items(self): """Return items sorted according to keys""" return [(k, self[k]) for k in self.keys()]
[docs] def values(self): """Return values sorted according to keys""" return [self[k] for k in self.keys()]
[docs] def keys(self): """Return sorted keys""" return list(iter(self))
def __iter__(self): """Return sorted keys""" return iter(sorted(super(Space, self).keys())) @property def configuration(self): """Return a dictionary of priors.""" return {name: dim.get_prior_string() for name, dim in self.items()} @property def cardinality(self): """Return the number of all all possible sets of samples in the space""" capacities = 1 for dim in self.values(): capacities *= dim.cardinality return capacities