from __future__ import annotations
import warnings
from typing import TYPE_CHECKING, Any, Iterator, KeysView, Mapping, Sequence
import numpy as np
from ConfigSpace import c_util
from ConfigSpace.exceptions import HyperparameterNotFoundError, IllegalValueError
from ConfigSpace.hyperparameters import FloatHyperparameter
if TYPE_CHECKING:
from ConfigSpace.configuration_space import ConfigurationSpace
[docs]class Configuration(Mapping[str, Any]):
def __init__(
self,
configuration_space: ConfigurationSpace,
values: Mapping[str, str | float | int | None] | None = None,
vector: Sequence[float] | np.ndarray | None = None,
allow_inactive_with_values: bool = False,
origin: Any | None = None,
config_id: int | None = None,
) -> None:
"""Class for a single configuration.
The :class:`~ConfigSpace.configuration_space.Configuration` object holds
for all active hyperparameters a value. While the
:class:`~ConfigSpace.configuration_space.ConfigurationSpace` stores the
definitions for the hyperparameters (value ranges, constraints,...), a
:class:`~ConfigSpace.configuration_space.Configuration` object is
more an instance of it. Parameters of a
:class:`~ConfigSpace.configuration_space.Configuration` object can be
accessed and modified similar to python dictionaries
(c.f. :ref:`Guide<1st_Example>`).
Parameters
----------
configuration_space : :class:`~ConfigSpace.configuration_space.ConfigurationSpace`
values : dict, optional
A dictionary with pairs (hyperparameter_name, value), where value is
a legal value of the hyperparameter in the above configuration_space
vector : np.ndarray, optional
A numpy array for efficient representation. Either values or vector
has to be given
allow_inactive_with_values : bool, optional
Whether an Exception will be raised if a value for an inactive
hyperparameter is given. Default is to raise an Exception.
Default to False
origin : Any, optional
Store information about the origin of this configuration. Defaults to None
config_id : int, optional
Integer configuration ID which can be used by a program using the ConfigSpace
package.
"""
if values is not None and vector is not None or values is None and vector is None:
raise ValueError("Specify Configuration as either a dictionary or a vector.")
self.config_space = configuration_space
self.allow_inactive_with_values = allow_inactive_with_values
self.origin = origin
self.config_id = config_id
# This is cached. When it's None, it means it needs to be relaoaded
# which is primarly handled in __getitem__.
self._values: dict[str, Any] | None = None
# Will be set below
self._vector: np.ndarray
if values is not None:
unknown_keys = values.keys() - self.config_space._hyperparameters.keys()
if any(unknown_keys):
raise ValueError(f"Unknown hyperparameter(s) {unknown_keys}")
# Using cs._hyperparameters to iterate makes sure that the hyperparameters in
# the configuration are sorted in the same way as they are sorted in
# the configuration space
self._values = {}
self._vector = np.ndarray(shape=len(configuration_space), dtype=float)
for i, (key, hp) in enumerate(configuration_space.items()):
value = values.get(key)
if value is None:
self._vector[i] = np.nan # By default, represent None values as NaN
continue
if not hp.is_legal(value):
raise IllegalValueError(hp, value)
# Truncate the float to be of constant length for a python version
if isinstance(hp, FloatHyperparameter):
value = float(repr(value))
self._values[key] = value
self._vector[i] = hp._inverse_transform(value)
self.is_valid_configuration()
elif vector is not None:
_vector = np.asarray(vector, dtype=float)
# If we have a 2d array with shape (n, 1), flatten it
if len(_vector.shape) == 2 and _vector.shape[1] == 1:
_vector = _vector.flatten()
if len(_vector.shape) > 1:
raise ValueError(
"Only 1d arrays can be converted to a Configuration, "
f"you passed an array of shape {_vector.shape}",
)
n_hyperparameters = len(self.config_space)
if len(_vector) != len(self.config_space):
raise ValueError(
f"Expected array of length {n_hyperparameters}, got {len(_vector)}",
)
self._vector = _vector
[docs] def is_valid_configuration(self) -> None:
"""Check if the object is a valid.
Raises
------
ValueError: If configuration is not valid.
"""
c_util.check_configuration(
self.config_space,
self._vector,
allow_inactive_with_values=self.allow_inactive_with_values,
)
[docs] def get_array(self) -> np.ndarray:
"""The internal vector representation of this config.
All continuous values are scaled between zero and one.
Returns
-------
numpy.ndarray
The vector representation of the configuration
"""
return self._vector
def __contains__(self, item: object) -> bool:
if not isinstance(item, str):
return False
return item in self
def __setitem__(self, key: str, value: Any) -> None:
param = self.config_space[key]
if not param.is_legal(value):
raise IllegalValueError(param, value)
idx = self.config_space._hyperparameter_idx[key]
# Recalculate the vector with respect to this new value
vector_value = param._inverse_transform(value)
new_array = c_util.change_hp_value(
self.config_space,
self.get_array().copy(),
param.name,
vector_value,
idx,
)
c_util.check_configuration(self.config_space, new_array, False)
# Reset cached items
self._vector = new_array
self._values = None
def __getitem__(self, key: str) -> Any:
if self._values is not None and key in self._values:
return self._values[key]
if key not in self.config_space:
raise HyperparameterNotFoundError(key, space=self.config_space)
item_idx = self.config_space._hyperparameter_idx[key]
raw_value = self._vector[item_idx]
if not np.isfinite(raw_value):
# NOTE: Techinically we could raise an `InactiveHyperparameterError` here
# but that causes the `.get()` method from being a mapping to fail.
# Normally `config.get(key)`, if it fails, will return None. Apparently,
# this only works if `__getitem__[]` raises a KeyError or something derived
# from it.
raise KeyError(key)
hyperparameter = self.config_space._hyperparameters[key]
value = hyperparameter._transform(raw_value)
# Truncate float to be of constant length for a python version
if isinstance(hyperparameter, FloatHyperparameter):
value = float(repr(value))
if self._values is None:
self._values = {}
self._values[key] = value
return value
[docs] def keys(self) -> KeysView[str]:
"""Return the keys of the configuration.
Returns
-------
KeysView[str]
The keys of the configuration
"""
d = {
key: self._vector[idx]
for idx, key in enumerate(self.config_space.keys())
if np.isfinite(self._vector[idx])
}
return d.keys()
def __eq__(self, other: Any) -> bool:
if isinstance(other, self.__class__):
return dict(self) == dict(other) and self.config_space == other.config_space
return NotImplemented
def __hash__(self) -> int:
return hash(self.__repr__())
def __repr__(self) -> str:
values = dict(self)
header = "Configuration(values={"
lines = [f" '{key}': {repr(values[key])}," for key in sorted(values.keys())]
end = "})"
return "\n".join([header, *lines, end])
def __iter__(self) -> Iterator[str]:
return iter(self.keys())
def __len__(self) -> int:
return len(self.config_space)
# ------------ Marked Deprecated --------------------
# Probably best to only remove these once we actually
# make some other breaking changes
# * Search `Marked Deprecated` to find others
[docs] def get_dictionary(self) -> dict[str, Any]:
"""A representation of the :class:`~ConfigSpace.configuration_space.Configuration`
in dictionary form.
Returns
-------
dict
Configuration as dictionary
"""
warnings.warn(
"`Configuration` act's like a dictionary."
" Please use `dict(config)` instead of `get_dictionary`"
" if you explicitly need a `dict`",
DeprecationWarning,
stacklevel=2,
)
return dict(self)
# ---------------------------------------------------