From ff0f2b256174c8bb29f30604f64e5f3d94821ed3 Mon Sep 17 00:00:00 2001 From: eddiebergman Date: Mon, 18 Dec 2023 15:30:56 +0100 Subject: [PATCH 001/104] uncythonize, pytest update --- .gitignore | 1 + ConfigSpace/{c_util.pyx => c_util.py} | 185 +- ConfigSpace/{conditions.pyx => conditions.py} | 360 +- ConfigSpace/configuration_space.py | 4 +- ConfigSpace/{forbidden.pyx => forbidden.py} | 342 +- .../{beta_float.pyx => beta_float.py} | 140 +- .../{beta_integer.pyx => beta_integer.py} | 135 +- .../{categorical.pyx => categorical.py} | 47 +- .../{constant.pyx => constant.py} | 91 +- ...rparameter.pyx => float_hyperparameter.py} | 34 +- .../{hyperparameter.pyx => hyperparameter.py} | 14 +- ...arameter.pyx => integer_hyperparameter.py} | 45 +- .../{normal_float.pyx => normal_float.py} | 182 +- .../{normal_integer.pyx => normal_integer.py} | 187 +- .../{numerical.pyx => numerical.py} | 51 +- .../{ordinal.pyx => ordinal.py} | 111 +- .../{uniform_float.pyx => uniform_float.py} | 90 +- ...uniform_integer.pyx => uniform_integer.py} | 178 +- pyproject.toml | 12 +- setup.py | 6 +- test/read_and_write/test_json.py | 204 +- test/read_and_write/test_pcs_converter.py | 1091 +-- test/test_conditions.py | 625 +- test/test_configuration_space.py | 2320 +++---- .../test_sample_configuration_spaces.py | 88 +- test/test_forbidden.py | 497 +- test/test_hyperparameters.py | 5946 +++++++++-------- test/test_util.py | 1119 ++-- 28 files changed, 7107 insertions(+), 6998 deletions(-) rename ConfigSpace/{c_util.pyx => c_util.py} (74%) rename ConfigSpace/{conditions.pyx => conditions.py} (68%) rename ConfigSpace/{forbidden.pyx => forbidden.py} (62%) rename ConfigSpace/hyperparameters/{beta_float.pyx => beta_float.py} (70%) rename ConfigSpace/hyperparameters/{beta_integer.pyx => beta_integer.py} (69%) rename ConfigSpace/hyperparameters/{categorical.pyx => categorical.py} (91%) rename ConfigSpace/hyperparameters/{constant.pyx => constant.py} (64%) rename ConfigSpace/hyperparameters/{float_hyperparameter.pyx => float_hyperparameter.py} (78%) rename ConfigSpace/hyperparameters/{hyperparameter.pyx => hyperparameter.py} (94%) rename ConfigSpace/hyperparameters/{integer_hyperparameter.pyx => integer_hyperparameter.py} (73%) rename ConfigSpace/hyperparameters/{normal_float.pyx => normal_float.py} (66%) rename ConfigSpace/hyperparameters/{normal_integer.pyx => normal_integer.py} (70%) rename ConfigSpace/hyperparameters/{numerical.pyx => numerical.py} (59%) rename ConfigSpace/hyperparameters/{ordinal.pyx => ordinal.py} (80%) rename ConfigSpace/hyperparameters/{uniform_float.pyx => uniform_float.py} (74%) rename ConfigSpace/hyperparameters/{uniform_integer.pyx => uniform_integer.py} (68%) diff --git a/.gitignore b/.gitignore index 8bf83c8a..b56c1500 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ docs/examples/* # C extensions *.so *.c +*.jukit # Packages *.egg diff --git a/ConfigSpace/c_util.pyx b/ConfigSpace/c_util.py similarity index 74% rename from ConfigSpace/c_util.pyx rename to ConfigSpace/c_util.py index 1f028305..5603c866 100644 --- a/ConfigSpace/c_util.pyx +++ b/ConfigSpace/c_util.py @@ -1,36 +1,24 @@ +from __future__ import annotations + from collections import deque import numpy as np -from ConfigSpace.forbidden import AbstractForbiddenComponent -from ConfigSpace.forbidden cimport AbstractForbiddenComponent -from ConfigSpace.hyperparameters import Hyperparameter -from ConfigSpace.hyperparameters.hyperparameter cimport Hyperparameter -from ConfigSpace.conditions import ConditionComponent -from ConfigSpace.conditions cimport ConditionComponent -from ConfigSpace.conditions import OrConjunction + +from ConfigSpace.conditions import ConditionComponent, OrConjunction from ConfigSpace.exceptions import ( + ActiveHyperparameterNotSetError, ForbiddenValueError, IllegalValueError, - ActiveHyperparameterNotSetError, InactiveHyperparameterSetError, ) - -from libc.stdlib cimport malloc, free -cimport numpy as np - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t +from ConfigSpace.forbidden import AbstractForbiddenComponent +from ConfigSpace.hyperparameters import Hyperparameter +from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter -cpdef int check_forbidden(list forbidden_clauses, np.ndarray vector) except 1: - cdef int Iforbidden = len(forbidden_clauses) - cdef AbstractForbiddenComponent clause +def check_forbidden(forbidden_clauses: list, vector: np.ndarray) -> int: + Iforbidden: int = len(forbidden_clauses) + clause: AbstractForbiddenComponent for i in range(Iforbidden): clause = forbidden_clauses[i] @@ -38,27 +26,24 @@ raise ForbiddenValueError("Given vector violates forbidden clause %s" % (str(clause))) -cpdef int check_configuration( +def check_configuration( self, - np.ndarray vector, - bint allow_inactive_with_values -) except 1: - cdef str hp_name - cdef Hyperparameter hyperparameter - cdef int hyperparameter_idx - cdef DTYPE_t hp_value - cdef int add - cdef ConditionComponent condition - cdef Hyperparameter child - cdef list conditions - cdef list children - cdef set inactive - cdef set visited - - cdef int* active - active = malloc(sizeof(int) * len(vector)) - for i in range(len(vector)): - active[i] = 0 + vector: np.ndarray, + allow_inactive_with_values: bool, +) -> int: + hp_name: str + hyperparameter: Hyperparameter + hyperparameter_idx: int + hp_value: float | int + add: int + condition: ConditionComponent + child: Hyperparameter + conditions: list + children: list + inactive: set + visited: set + + active: np.ndarray = np.zeros(len(vector), dtype=int) unconditional_hyperparameters = self.get_all_unconditional_hyperparameters() to_visit = deque() @@ -77,7 +62,6 @@ hp_value = vector[hp_idx] if not np.isnan(hp_value) and not hyperparameter.is_legal_vector(hp_value): - free(active) raise IllegalValueError(hyperparameter, hp_value) children = self._children_of[hp_name] @@ -96,58 +80,50 @@ to_visit.appendleft(child.name) if active[hp_idx] and np.isnan(hp_value): - free(active) raise ActiveHyperparameterNotSetError(hyperparameter) for hp_idx in self._idx_to_hyperparameter: - if not allow_inactive_with_values and not active[hp_idx] and not np.isnan(vector[hp_idx]): # Only look up the value (in the line above) if the hyperparameter is inactive! hp_name = self._idx_to_hyperparameter[hp_idx] hp_value = vector[hp_idx] - free(active) raise InactiveHyperparameterSetError(hyperparameter, hp_value) - free(active) self._check_forbidden(vector) -cpdef np.ndarray correct_sampled_array( - np.ndarray[DTYPE_t, ndim=1] vector, - list forbidden_clauses_unconditionals, - list forbidden_clauses_conditionals, - list hyperparameters_with_children, - int num_hyperparameters, - list unconditional_hyperparameters, - dict hyperparameter_to_idx, - dict parent_conditions_of, - dict parents_of, - dict children_of, -): - cdef AbstractForbiddenComponent clause - cdef ConditionComponent condition - cdef int hyperparameter_idx - cdef DTYPE_t NaN = np.NaN - cdef set visited - cdef set inactive - cdef Hyperparameter child - cdef list children - cdef str child_name - cdef list parents - cdef Hyperparameter parent - cdef int parents_visited - cdef list conditions - cdef int add - - cdef int* active - active = malloc(sizeof(int) * num_hyperparameters) - for j in range(num_hyperparameters): - active[j] = 0 +def correct_sampled_array( + vector: np.ndarray, + forbidden_clauses_unconditionals: list, + forbidden_clauses_conditionals: list, + hyperparameters_with_children: list, + num_hyperparameters: int, + unconditional_hyperparameters: list, + hyperparameter_to_idx: dict, + parent_conditions_of: dict, + parents_of: dict, + children_of: dict, +) -> np.ndarray: + clause: AbstractForbiddenComponent + condition: ConditionComponent + hyperparameter_idx: int + NaN: float = np.NaN + visited: set + inactive: set + child: Hyperparameter + children: list + child_name: str + parents: list + parent: Hyperparameter + parents_visited: int + conditions: list + add: int + + active: np.ndarray = np.zeros(len(vector), dtype=int) for j in range(len(forbidden_clauses_unconditionals)): clause = forbidden_clauses_unconditionals[j] if clause.c_is_forbidden_vector(vector, strict=False): - free(active) msg = "Given vector violates forbidden clause %s" % str(clause) raise ForbiddenValueError(msg) @@ -216,7 +192,6 @@ if not active[j]: vector[j] = NaN - free(active) for j in range(len(forbidden_clauses_conditionals)): clause = forbidden_clauses_conditionals[j] if clause.c_is_forbidden_vector(vector, strict=False): @@ -226,13 +201,13 @@ return vector -cpdef np.ndarray change_hp_value( +def change_hp_value( configuration_space, - np.ndarray[DTYPE_t, ndim=1] configuration_array, - str hp_name, - DTYPE_t hp_value, - int index, -): + configuration_array: np.ndarray, + hp_name: str, + hp_value: float, + index: int, +) -> np.ndarray: """Change hyperparameter value in configuration array to given value. Does not check if the new value is legal. Activates and deactivates other @@ -255,23 +230,23 @@ ------- np.ndarray """ - cdef Hyperparameter current - cdef str current_name - cdef list disabled - cdef set hps_to_be_activate - cdef set visited - cdef int active - cdef ConditionComponent condition - cdef int current_idx - cdef DTYPE_t current_value - cdef DTYPE_t default_value - cdef list children - cdef list children_ - cdef Hyperparameter ch - cdef str child - cdef set to_disable - cdef DTYPE_t NaN = np.NaN - cdef dict children_of = configuration_space._children_of + current: Hyperparameter + current_name: str + disabled: list + hps_to_be_activate: set + visited: set + active: int + condition: ConditionComponent + current_idx: int + current_value: float + default_value: float + children: list + children_: list + ch: Hyperparameter + child: str + to_disable: set + NaN: float = np.NaN + children_of: dict = configuration_space._children_of configuration_array[index] = hp_value @@ -321,7 +296,7 @@ if current_name in disabled: continue - if active and not current_value == current_value: + if active and current_value != current_value: default_value = current.normalized_default_value configuration_array[current_idx] = default_value children_ = children_of[current_name] diff --git a/ConfigSpace/conditions.pyx b/ConfigSpace/conditions.py similarity index 68% rename from ConfigSpace/conditions.pyx rename to ConfigSpace/conditions.py index 90e71c26..475c74ef 100644 --- a/ConfigSpace/conditions.pyx +++ b/ConfigSpace/conditions.py @@ -25,23 +25,20 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from __future__ import annotations -import io import copy +import io from itertools import combinations -from typing import Any, List, Union, Tuple, Dict - -from libc.stdlib cimport malloc, free +from typing import TYPE_CHECKING, Any import numpy as np -from ConfigSpace.hyperparameters.hyperparameter cimport Hyperparameter - -cimport numpy as np +if TYPE_CHECKING: + from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter -cdef class ConditionComponent(object): - +class ConditionComponent: def __init__(self) -> None: pass @@ -51,50 +48,43 @@ def __repr__(self) -> str: def set_vector_idx(self, hyperparameter_to_idx) -> None: pass - def get_children_vector(self) -> List[int]: + def get_children_vector(self) -> list[int]: pass - def get_parents_vector(self) -> List[int]: + def get_parents_vector(self) -> list[int]: pass - def get_children(self) -> List["ConditionComponent"]: + def get_children(self) -> list[ConditionComponent]: pass - def get_parents(self) -> List["ConditionComponent"]: + def get_parents(self) -> list[ConditionComponent]: pass - def get_descendant_literal_conditions(self) ->List["AbstractCondition"]: + def get_descendant_literal_conditions(self) -> list[AbstractCondition]: pass - def evaluate(self, - instantiated_parent_hyperparameter: Dict[str, Union[None, int, float, str]] - ) -> bool: + def evaluate( + self, + instantiated_parent_hyperparameter: dict[str, None | int | float | str], + ) -> bool: pass def evaluate_vector(self, instantiated_vector): return bool(self._evaluate_vector(instantiated_vector)) - cdef int _evaluate_vector(self, np.ndarray value): + def _evaluate_vector(self, value: np.ndarray) -> int: pass def __hash__(self) -> int: - """Override the default hash behavior (that returns the id or the object)""" + """Override the default hash behavior (that returns the id or the object).""" return hash(tuple(sorted(self.__dict__.items()))) -cdef class AbstractCondition(ConditionComponent): - cdef public Hyperparameter child - cdef public Hyperparameter parent - cdef public int child_vector_id - cdef public int parent_vector_id - cdef public value - cdef public DTYPE_t vector_value - +class AbstractCondition(ConditionComponent): def __init__(self, child: Hyperparameter, parent: Hyperparameter) -> None: if child == parent: raise ValueError( - "The child and parent hyperparameter must be different " - "hyperparameters." + "The child and parent hyperparameter must be different " "hyperparameters.", ) self.child = child self.parent = parent @@ -127,42 +117,47 @@ def set_vector_idx(self, hyperparameter_to_idx: dict): self.child_vector_id = hyperparameter_to_idx[self.child.name] self.parent_vector_id = hyperparameter_to_idx[self.parent.name] - def get_children_vector(self) -> List[int]: + def get_children_vector(self) -> list[int]: return [self.child_vector_id] - def get_parents_vector(self) -> List[int]: + def get_parents_vector(self) -> list[int]: return [self.parent_vector_id] - def get_children(self) -> List[Hyperparameter]: + def get_children(self) -> list[Hyperparameter]: return [self.child] - def get_parents(self) -> List[Hyperparameter]: + def get_parents(self) -> list[Hyperparameter]: return [self.parent] - def get_descendant_literal_conditions(self) -> List["AbstractCondition"]: + def get_descendant_literal_conditions(self) -> list[AbstractCondition]: return [self] - def evaluate(self, instantiated_parent_hyperparameter: Dict[str, Union[int, float, str]] - ) -> bool: + def evaluate( + self, + instantiated_parent_hyperparameter: dict[str, int | float | str], + ) -> bool: hp_name = self.parent.name return self._evaluate(instantiated_parent_hyperparameter[hp_name]) - cdef int _evaluate_vector(self, np.ndarray instantiated_vector): + def _evaluate_vector(self, instantiated_vector: np.ndarray) -> int: if self.parent_vector_id is None: raise ValueError("Parent vector id should not be None when calling evaluate vector") return self._inner_evaluate_vector(instantiated_vector[self.parent_vector_id]) - def _evaluate(self, instantiated_parent_hyperparameter: Union[str, int, float]) -> bool: + def _evaluate(self, instantiated_parent_hyperparameter: str | int | float) -> bool: pass - cdef int _inner_evaluate_vector(self, DTYPE_t value): + def _inner_evaluate_vector(self, value) -> int: pass -cdef class EqualsCondition(AbstractCondition): - - def __init__(self, child: Hyperparameter, parent: Hyperparameter, - value: Union[str, float, int]) -> None: +class EqualsCondition(AbstractCondition): + def __init__( + self, + child: Hyperparameter, + parent: Hyperparameter, + value: str | float | int, + ) -> None: """Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *equal* to ``value``. @@ -188,19 +183,18 @@ def __init__(self, child: Hyperparameter, parent: Hyperparameter, value : str, float, int Value, which the parent is compared to """ - - super(EqualsCondition, self).__init__(child, parent) + super().__init__(child, parent) if not parent.is_legal(value): - raise ValueError("Hyperparameter '%s' is " - "conditional on the illegal value '%s' of " - "its parent hyperparameter '%s'" % - (child.name, value, parent.name)) + raise ValueError( + "Hyperparameter '{}' is " + "conditional on the illegal value '{}' of " + "its parent hyperparameter '{}'".format(child.name, value, parent.name), + ) self.value = value self.vector_value = self.parent._inverse_transform(self.value) def __repr__(self) -> str: - return "%s | %s == %s" % (self.child.name, self.parent.name, - repr(self.value)) + return f"{self.child.name} | {self.parent.name} == {self.value!r}" def __copy__(self): return self.__class__( @@ -209,30 +203,28 @@ def __copy__(self): value=copy.copy(self.value), ) - def _evaluate(self, value: Union[str, float, int]) -> bool: + def _evaluate(self, value: str | float | int) -> bool: # No need to check if the value to compare is a legal value; either it # is equal (and thus legal), or it would evaluate to False anyway cmp = self.parent.compare(value, self.value) - if cmp == 0: - return True - else: - return False + return cmp == 0 - cdef int _inner_evaluate_vector(self, DTYPE_t value): + def _inner_evaluate_vector(self, value) -> int: # No need to check if the value to compare is a legal value; either it # is equal (and thus legal), or it would evaluate to False anyway - cdef int cmp = self.parent.compare_vector(value, self.vector_value) - if cmp == 0: - return True - else: - return False + cmp = self.parent.compare_vector(value, self.vector_value) + return cmp == 0 -cdef class NotEqualsCondition(AbstractCondition): - def __init__(self, child: Hyperparameter, parent: Hyperparameter, - value: Union[str, float, int]) -> None: +class NotEqualsCondition(AbstractCondition): + def __init__( + self, + child: Hyperparameter, + parent: Hyperparameter, + value: str | float | int, + ) -> None: """Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *not equal* to ``value``. @@ -259,18 +251,18 @@ def __init__(self, child: Hyperparameter, parent: Hyperparameter, value : str, float, int Value, which the parent is compared to """ - super(NotEqualsCondition, self).__init__(child, parent) + super().__init__(child, parent) if not parent.is_legal(value): - raise ValueError("Hyperparameter '%s' is " - "conditional on the illegal value '%s' of " - "its parent hyperparameter '%s'" % - (child.name, value, parent.name)) + raise ValueError( + "Hyperparameter '{}' is " + "conditional on the illegal value '{}' of " + "its parent hyperparameter '{}'".format(child.name, value, parent.name), + ) self.value = value self.vector_value = self.parent._inverse_transform(self.value) def __repr__(self) -> str: - return "%s | %s != %s" % (self.child.name, self.parent.name, - repr(self.value)) + return f"{self.child.name} | {self.parent.name} != {self.value!r}" def __copy__(self): return self.__class__( @@ -279,30 +271,28 @@ def __copy__(self): value=copy.copy(self.value), ) - def _evaluate(self, value: Union[str, float, int]) -> bool: + def _evaluate(self, value: str | float | int) -> bool: if not self.parent.is_legal(value): return False cmp = self.parent.compare(value, self.value) - if cmp != 0: - return True - else: - return False + return cmp != 0 - cdef int _inner_evaluate_vector(self, DTYPE_t value): + def _inner_evaluate_vector(self, value) -> int: if not self.parent.is_legal_vector(value): return False - cdef int cmp = self.parent.compare_vector(value, self.vector_value) - if cmp != 0: - return True - else: - return False + cmp = self.parent.compare_vector(value, self.vector_value) + return cmp != 0 -cdef class LessThanCondition(AbstractCondition): - def __init__(self, child: Hyperparameter, parent: Hyperparameter, - value: Union[str, float, int]) -> None: +class LessThanCondition(AbstractCondition): + def __init__( + self, + child: Hyperparameter, + parent: Hyperparameter, + value: str | float | int, + ) -> None: """ Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *less than* ``value``. @@ -329,19 +319,19 @@ def __init__(self, child: Hyperparameter, parent: Hyperparameter, value : str, float, int Value, which the parent is compared to """ - super(LessThanCondition, self).__init__(child, parent) + super().__init__(child, parent) self.parent.allow_greater_less_comparison() if not parent.is_legal(value): - raise ValueError("Hyperparameter '%s' is " - "conditional on the illegal value '%s' of " - "its parent hyperparameter '%s'" % - (child.name, value, parent.name)) + raise ValueError( + "Hyperparameter '{}' is " + "conditional on the illegal value '{}' of " + "its parent hyperparameter '{}'".format(child.name, value, parent.name), + ) self.value = value self.vector_value = self.parent._inverse_transform(self.value) def __repr__(self) -> str: - return "%s | %s < %s" % (self.child.name, self.parent.name, - repr(self.value)) + return f"{self.child.name} | {self.parent.name} < {self.value!r}" def __copy__(self): return self.__class__( @@ -350,30 +340,28 @@ def __copy__(self): value=copy.copy(self.value), ) - def _evaluate(self, value: Union[str, float, int]) -> bool: + def _evaluate(self, value: str | float | int) -> bool: if not self.parent.is_legal(value): return False cmp = self.parent.compare(value, self.value) - if cmp == -1: - return True - else: - return False + return cmp == -1 - cdef int _inner_evaluate_vector(self, DTYPE_t value): + def _inner_evaluate_vector(self, value) -> int: if not self.parent.is_legal_vector(value): return False - cdef int cmp = self.parent.compare_vector(value, self.vector_value) - if cmp == -1: - return True - else: - return False + cmp = self.parent.compare_vector(value, self.vector_value) + return cmp == -1 -cdef class GreaterThanCondition(AbstractCondition): - def __init__(self, child: Hyperparameter, parent: Hyperparameter, - value: Union[str, float, int]) -> None: +class GreaterThanCondition(AbstractCondition): + def __init__( + self, + child: Hyperparameter, + parent: Hyperparameter, + value: str | float | int, + ) -> None: """ Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *greater than* ``value``. @@ -400,20 +388,20 @@ def __init__(self, child: Hyperparameter, parent: Hyperparameter, value : str, float, int Value, which the parent is compared to """ - super(GreaterThanCondition, self).__init__(child, parent) + super().__init__(child, parent) self.parent.allow_greater_less_comparison() if not parent.is_legal(value): - raise ValueError("Hyperparameter '%s' is " - "conditional on the illegal value '%s' of " - "its parent hyperparameter '%s'" % - (child.name, value, parent.name)) + raise ValueError( + "Hyperparameter '{}' is " + "conditional on the illegal value '{}' of " + "its parent hyperparameter '{}'".format(child.name, value, parent.name), + ) self.value = value self.vector_value = self.parent._inverse_transform(self.value) def __repr__(self) -> str: - return "%s | %s > %s" % (self.child.name, self.parent.name, - repr(self.value)) + return f"{self.child.name} | {self.parent.name} > {self.value!r}" def __copy__(self): return self.__class__( @@ -422,32 +410,28 @@ def __copy__(self): value=copy.copy(self.value), ) - def _evaluate(self, value: Union[str, float, int]) -> bool: + def _evaluate(self, value: None | str | float | int) -> bool: if not self.parent.is_legal(value): return False cmp = self.parent.compare(value, self.value) - if cmp == 1: - return True - else: - return False + return cmp == 1 - cdef int _inner_evaluate_vector(self, DTYPE_t value): + def _inner_evaluate_vector(self, value) -> int: if not self.parent.is_legal_vector(value): return False - cdef int cmp = self.parent.compare_vector(value, self.vector_value) - if cmp == 1: - return True - else: - return False + cmp = self.parent.compare_vector(value, self.vector_value) + return cmp == 1 -cdef class InCondition(AbstractCondition): - cdef public values - cdef public vector_values - def __init__(self, child: Hyperparameter, parent: Hyperparameter, - values: List[Union[str, float, int]]) -> None: +class InCondition(AbstractCondition): + def __init__( + self, + child: Hyperparameter, + parent: Hyperparameter, + values: list[str | float | int], + ) -> None: """ Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *in* a set of ``values``. @@ -475,36 +459,35 @@ def __init__(self, child: Hyperparameter, parent: Hyperparameter, Collection of values, which the parent is compared to """ - super(InCondition, self).__init__(child, parent) + super().__init__(child, parent) for value in values: if not parent.is_legal(value): - raise ValueError("Hyperparameter '%s' is " - "conditional on the illegal value '%s' of " - "its parent hyperparameter '%s'" % - (child.name, value, parent.name)) + raise ValueError( + "Hyperparameter '{}' is " + "conditional on the illegal value '{}' of " + "its parent hyperparameter '{}'".format(child.name, value, parent.name), + ) self.values = values self.value = values self.vector_values = [self.parent._inverse_transform(value) for value in self.values] def __repr__(self) -> str: - return "%s | %s in {%s}" % (self.child.name, self.parent.name, - ", ".join( - [repr(value) for value in self.values])) + return "{} | {} in {{{}}}".format( + self.child.name, + self.parent.name, + ", ".join([repr(value) for value in self.values]), + ) - def _evaluate(self, value: Union[str, float, int]) -> bool: + def _evaluate(self, value: str | float | int) -> bool: return value in self.values - cdef int _inner_evaluate_vector(self, DTYPE_t value): + def _inner_evaluate_vector(self, value) -> int: return value in self.vector_values -cdef class AbstractConjunction(ConditionComponent): - cdef public tuple components - cdef int n_components - cdef tuple dlcs - +class AbstractConjunction(ConditionComponent): def __init__(self, *args: AbstractCondition) -> None: - super(AbstractConjunction, self).__init__() + super().__init__() self.components = args self.n_components = len(self.components) self.dlcs = self.get_descendant_literal_conditions() @@ -512,16 +495,16 @@ def __init__(self, *args: AbstractCondition) -> None: # Test the classes for idx, component in enumerate(self.components): if not isinstance(component, ConditionComponent): - raise TypeError("Argument #%d is not an instance of %s, " - "but %s" % ( - idx, ConditionComponent, type(component))) + raise TypeError( + "Argument #%d is not an instance of %s, " + "but %s" % (idx, ConditionComponent, type(component)), + ) # Test that all conjunctions and conditions have the same child! children = self.get_children() for c1, c2 in combinations(children, 2): if c1 != c2: - raise ValueError("All Conjunctions and Conditions must have " - "the same child.") + raise ValueError("All Conjunctions and Conditions must have " "the same child.") def __eq__(self, other: Any) -> bool: """ @@ -543,7 +526,7 @@ def __eq__(self, other: Any) -> bool: return False for component, other_component in zip(self.components, other.components): - if (component != other_component): + if component != other_component: return False return True @@ -551,7 +534,7 @@ def __eq__(self, other: Any) -> bool: def __copy__(self): return self.__class__(*[copy.copy(comp) for comp in self.components]) - def get_descendant_literal_conditions(self) -> Tuple[AbstractCondition]: + def get_descendant_literal_conditions(self) -> tuple[AbstractCondition]: children = [] # type: List[AbstractCondition] for component in self.components: if isinstance(component, AbstractConjunction): @@ -564,77 +547,72 @@ def set_vector_idx(self, hyperparameter_to_idx: dict): for component in self.components: component.set_vector_idx(hyperparameter_to_idx) - def get_children_vector(self) -> List[int]: + def get_children_vector(self) -> list[int]: children_vector = [] for component in self.components: children_vector.extend(component.get_children_vector()) return children_vector - def get_parents_vector(self) -> List[int]: + def get_parents_vector(self) -> list[int]: parents_vector = [] for component in self.components: parents_vector.extend(component.get_parents_vector()) return parents_vector - def get_children(self) -> List[ConditionComponent]: + def get_children(self) -> list[ConditionComponent]: children = [] # type: List[ConditionComponent] for component in self.components: children.extend(component.get_children()) return children - def get_parents(self) -> List[ConditionComponent]: + def get_parents(self) -> list[ConditionComponent]: parents = [] # type: List[ConditionComponent] for component in self.components: parents.extend(component.get_parents()) return parents - def evaluate(self, instantiated_hyperparameters: Dict[str, Union[None, int, float, str]] - ) -> bool: - cdef int* arrptr - arrptr = malloc(sizeof(int) * self.n_components) + def evaluate( + self, + instantiated_hyperparameters: dict[str, None | int | float | str], + ) -> bool: + values = np.empty(self.n_components, dtype=np.int32) # Then, check if all parents were passed conditions = self.dlcs for condition in conditions: if condition.parent.name not in instantiated_hyperparameters: - raise ValueError("Evaluate must be called with all " - "instanstatiated parent hyperparameters in " - "the conjunction; you are (at least) missing " - "'%s'" % condition.parent.name) + raise ValueError( + "Evaluate must be called with all " + "instanstatiated parent hyperparameters in " + "the conjunction; you are (at least) missing " + "'%s'" % condition.parent.name, + ) # Finally, call evaluate for all direct descendents and combine the # outcomes for i, component in enumerate(self.components): e = component.evaluate(instantiated_hyperparameters) - arrptr[i] = (e) + values[i] = e - rval = self._evaluate(self.n_components, arrptr) - free(arrptr) - return rval + return self._evaluate(self.n_components, values) - cdef int _evaluate_vector(self, np.ndarray instantiated_vector): - cdef ConditionComponent component - cdef int e - cdef int rval - cdef int* arrptr - arrptr = malloc(sizeof(int) * self.n_components) + def _evaluate_vector(self, instantiated_vector: np.ndarray) -> int: + values = np.empty(self.n_components, dtype=np.int32) # Finally, call evaluate for all direct descendents and combine the # outcomes for i in range(self.n_components): component = self.components[i] e = component._evaluate_vector(instantiated_vector) - arrptr[i] = e + values[i] = e - rval = self._evaluate(self.n_components, arrptr) - free(arrptr) - return rval + return self._evaluate(self.n_components, values) - cdef int _evaluate(self, int I, int* evaluations): + def _evaluate(self, I: int, evaluations) -> int: pass -cdef class AndConjunction(AbstractConjunction): +class AndConjunction(AbstractConjunction): # TODO: test if an AndConjunction results in an illegal state or a # Tautology! -> SAT solver def __init__(self, *args: AbstractCondition) -> None: @@ -667,7 +645,7 @@ def __init__(self, *args: AbstractCondition) -> None: """ if len(args) < 2: raise ValueError("AndConjunction must at least have two Conditions.") - super(AndConjunction, self).__init__(*args) + super().__init__(*args) def __repr__(self) -> str: retval = io.StringIO() @@ -679,10 +657,7 @@ def __repr__(self) -> str: retval.write(")") return retval.getvalue() - cdef int _evaluate_vector(self, np.ndarray instantiated_vector): - cdef ConditionComponent component - cdef int e - + def _evaluate_vector(self, instantiated_vector: np.ndarray) -> int: for i in range(self.n_components): component = self.components[i] e = component._evaluate_vector(instantiated_vector) @@ -691,14 +666,14 @@ def __repr__(self) -> str: return 1 - cdef int _evaluate(self, int I, int* evaluations): + def _evaluate(self, I: int, evaluations) -> int: for i in range(I): if evaluations[i] == 0: return 0 return 1 -cdef class OrConjunction(AbstractConjunction): +class OrConjunction(AbstractConjunction): def __init__(self, *args: AbstractCondition) -> None: """ Similar to the *AndConjunction*, constraints can be combined by @@ -728,7 +703,7 @@ def __init__(self, *args: AbstractCondition) -> None: """ if len(args) < 2: raise ValueError("OrConjunction must at least have two Conditions.") - super(OrConjunction, self).__init__(*args) + super().__init__(*args) def __repr__(self) -> str: retval = io.StringIO() @@ -740,16 +715,13 @@ def __repr__(self) -> str: retval.write(")") return retval.getvalue() - cdef int _evaluate(self, int I, int* evaluations): + def _evaluate(self, I: int, evaluations) -> int: for i in range(I): if evaluations[i] == 1: return 1 return 0 - cdef int _evaluate_vector(self, np.ndarray instantiated_vector): - cdef ConditionComponent component - cdef int e - + def _evaluate_vector(self, instantiated_vector: np.ndarray) -> int: for i in range(self.n_components): component = self.components[i] e = component._evaluate_vector(instantiated_vector) diff --git a/ConfigSpace/configuration_space.py b/ConfigSpace/configuration_space.py index 29f624f6..3b65efd6 100644 --- a/ConfigSpace/configuration_space.py +++ b/ConfigSpace/configuration_space.py @@ -1140,8 +1140,8 @@ def __getitem__(self, key: str) -> Hyperparameter: return hp - def __contains__(self, key: str) -> bool: - return key in self._hyperparameters + #def __contains__(self, key: str) -> bool: + # return key in self._hyperparameters def __repr__(self) -> str: retval = io.StringIO() diff --git a/ConfigSpace/forbidden.pyx b/ConfigSpace/forbidden.py similarity index 62% rename from ConfigSpace/forbidden.pyx rename to ConfigSpace/forbidden.py index 9ba0dd30..19677228 100644 --- a/ConfigSpace/forbidden.pyx +++ b/ConfigSpace/forbidden.py @@ -25,22 +25,19 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from __future__ import annotations import copy -import numpy as np import io -from ConfigSpace.hyperparameters import Hyperparameter -from ConfigSpace.hyperparameters.hyperparameter cimport Hyperparameter -from typing import Dict, Any, Union +from typing import Any -from ConfigSpace.forbidden cimport AbstractForbiddenComponent - -from libc.stdlib cimport malloc, free -cimport numpy as np +import numpy as np +from ConfigSpace.forbidden import AbstractForbiddenComponent +from ConfigSpace.hyperparameters import Hyperparameter -cdef class AbstractForbiddenComponent(object): +class AbstractForbiddenComponent: def __init__(self): pass @@ -68,155 +65,162 @@ def __eq__(self, other: Any) -> bool: if other.value is None: other.value = other.values - return (self.value == other.value and - self.hyperparameter.name == other.hyperparameter.name) + return self.value == other.value and self.hyperparameter.name == other.hyperparameter.name def __hash__(self) -> int: - """Override the default hash behavior (that returns the id or the object)""" + """Override the default hash behavior (that returns the id or the object).""" return hash(tuple(sorted(self.__dict__.items()))) def __copy__(self): raise NotImplementedError() - cpdef get_descendant_literal_clauses(self): + def get_descendant_literal_clauses(self): pass - cpdef set_vector_idx(self, hyperparameter_to_idx): + def set_vector_idx(self, hyperparameter_to_idx): pass - cpdef is_forbidden(self, instantiated_hyperparameters, strict): + def is_forbidden(self, instantiated_hyperparameters, strict): pass def is_forbidden_vector(self, instantiated_hyperparameters, strict): return bool(self.c_is_forbidden_vector(instantiated_hyperparameters, strict)) - cdef int c_is_forbidden_vector(self, np.ndarray instantiated_hyperparameters, int strict): + def c_is_forbidden_vector(self, instantiated_hyperparameters: np.ndarray, strict: int) -> int: pass -cdef class AbstractForbiddenClause(AbstractForbiddenComponent): - +class AbstractForbiddenClause(AbstractForbiddenComponent): def __init__(self, hyperparameter: Hyperparameter): if not isinstance(hyperparameter, Hyperparameter): - raise TypeError("Argument 'hyperparameter' is not of type %s." % - Hyperparameter) + raise TypeError("Argument 'hyperparameter' is not of type %s." % Hyperparameter) self.hyperparameter = hyperparameter self.vector_id = -1 - cpdef get_descendant_literal_clauses(self): - return (self, ) + def get_descendant_literal_clauses(self): + return (self,) - cpdef set_vector_idx(self, hyperparameter_to_idx): + def set_vector_idx(self, hyperparameter_to_idx): self.vector_id = hyperparameter_to_idx[self.hyperparameter.name] -cdef class SingleValueForbiddenClause(AbstractForbiddenClause): +class SingleValueForbiddenClause(AbstractForbiddenClause): def __init__(self, hyperparameter: Hyperparameter, value: Any) -> None: - super(SingleValueForbiddenClause, self).__init__(hyperparameter) + super().__init__(hyperparameter) if not self.hyperparameter.is_legal(value): - raise ValueError("Forbidden clause must be instantiated with a " - "legal hyperparameter value for '%s', but got " - "'%s'" % (self.hyperparameter, str(value))) + raise ValueError( + "Forbidden clause must be instantiated with a " + "legal hyperparameter value for '{}', but got " + "'{}'".format(self.hyperparameter, str(value)), + ) self.value = value self.vector_value = self.hyperparameter._inverse_transform(self.value) def __copy__(self): return self.__class__( hyperparameter=copy.copy(self.hyperparameter), - value=self.value + value=self.value, ) - cpdef is_forbidden(self, instantiated_hyperparameters, strict): + def is_forbidden(self, instantiated_hyperparameters, strict) -> int: value = instantiated_hyperparameters.get(self.hyperparameter.name) if value is None: if strict: - raise ValueError("Is_forbidden must be called with the " - "instantiated hyperparameter in the " - "forbidden clause; you are missing " - "'%s'" % self.hyperparameter.name) + raise ValueError( + "Is_forbidden must be called with the " + "instantiated hyperparameter in the " + "forbidden clause; you are missing " + "'%s'" % self.hyperparameter.name, + ) else: return False return self._is_forbidden(value) - cdef int c_is_forbidden_vector(self, np.ndarray instantiated_vector, int strict): - cdef DTYPE_t value = instantiated_vector[self.vector_id] + def c_is_forbidden_vector(self, instantiated_vector: np.ndarray, strict: int) -> int: + value = instantiated_vector[self.vector_id] if value != value: if strict: - raise ValueError("Is_forbidden must be called with the " - "instantiated vector id in the " - "forbidden clause; you are missing " - "'%s'" % self.vector_id) + raise ValueError( + "Is_forbidden must be called with the " + "instantiated vector id in the " + "forbidden clause; you are missing " + "'%s'" % self.vector_id, + ) else: return False return self._is_forbidden_vector(value) - cdef int _is_forbidden(self, value): + def _is_forbidden(self, value) -> int: pass - cdef int _is_forbidden_vector(self, DTYPE_t value): + def _is_forbidden_vector(self, value) -> int: pass -cdef class MultipleValueForbiddenClause(AbstractForbiddenClause): - cdef public values - cdef public vector_values - +class MultipleValueForbiddenClause(AbstractForbiddenClause): def __init__(self, hyperparameter: Hyperparameter, values: Any) -> None: - super(MultipleValueForbiddenClause, self).__init__(hyperparameter) + super().__init__(hyperparameter) for value in values: if not self.hyperparameter.is_legal(value): - raise ValueError("Forbidden clause must be instantiated with a " - "legal hyperparameter value for '%s', but got " - "'%s'" % (self.hyperparameter, str(value))) + raise ValueError( + "Forbidden clause must be instantiated with a " + "legal hyperparameter value for '{}', but got " + "'{}'".format(self.hyperparameter, str(value)), + ) self.values = values - self.vector_values = [self.hyperparameter._inverse_transform(value) - for value in self.values] + self.vector_values = [ + self.hyperparameter._inverse_transform(value) for value in self.values + ] def __copy__(self): return self.__class__( hyperparameter=copy.copy(self.hyperparameter), - values=copy.deepcopy(self.values) + values=copy.deepcopy(self.values), ) - cpdef is_forbidden(self, instantiated_hyperparameters, strict): + def is_forbidden(self, instantiated_hyperparameters, strict) -> bool: value = instantiated_hyperparameters.get(self.hyperparameter.name) if value is None: if strict: - raise ValueError("Is_forbidden must be called with the " - "instantiated hyperparameter in the " - "forbidden clause; you are missing " - "'%s'." % self.hyperparameter.name) + raise ValueError( + "Is_forbidden must be called with the " + "instantiated hyperparameter in the " + "forbidden clause; you are missing " + "'%s'." % self.hyperparameter.name, + ) else: return False return self._is_forbidden(value) - cdef int c_is_forbidden_vector(self, np.ndarray instantiated_vector, int strict): - cdef DTYPE_t value = instantiated_vector[self.vector_id] + def c_is_forbidden_vector(self, instantiated_vector: np.ndarray, strict: int) -> int: + value = instantiated_vector[self.vector_id] if value != value: if strict: - raise ValueError("Is_forbidden must be called with the " - "instantiated vector id in the " - "forbidden clause; you are missing " - "'%s'" % self.vector_id) + raise ValueError( + "Is_forbidden must be called with the " + "instantiated vector id in the " + "forbidden clause; you are missing " + "'%s'" % self.vector_id, + ) else: return False return self._is_forbidden_vector(value) - cdef int _is_forbidden(self, value): + def _is_forbidden(self, value) -> int: pass - cdef int _is_forbidden_vector(self, DTYPE_t value): + def _is_forbidden_vector(self, value) -> int: pass -cdef class ForbiddenEqualsClause(SingleValueForbiddenClause): - """A ForbiddenEqualsClause +class ForbiddenEqualsClause(SingleValueForbiddenClause): + """A ForbiddenEqualsClause. It forbids a value from the value range of a hyperparameter to be *equal to* ``value``. @@ -239,19 +243,21 @@ def __copy__(self): """ def __repr__(self): - return "Forbidden: %s == %s" % (self.hyperparameter.name, - repr(self.value)) + return f"Forbidden: {self.hyperparameter.name} == {self.value!r}" - cdef int _is_forbidden(self, value): + def _is_forbidden(self, value) -> bool: return value == self.value - cdef int _is_forbidden_vector(self, DTYPE_t value): + def _is_forbidden_vector(self, value) -> bool: return value == self.vector_value -cdef class ForbiddenInClause(MultipleValueForbiddenClause): - def __init__(self, hyperparameter: Dict[str, Union[None, str, float, int]], - values: Any) -> None: +class ForbiddenInClause(MultipleValueForbiddenClause): + def __init__( + self, + hyperparameter: dict[str, None | str | float | int], + values: Any, + ) -> None: """A ForbiddenInClause. It forbids a value from the value range of a hyperparameter to be @@ -278,38 +284,33 @@ def __init__(self, hyperparameter: Dict[str, Union[None, str, float, int]], values : Any Collection of forbidden values """ - - super(ForbiddenInClause, self).__init__(hyperparameter, values) + super().__init__(hyperparameter, values) self.values = set(self.values) self.vector_values = set(self.vector_values) def __repr__(self) -> str: - return "Forbidden: %s in %s" % ( + return "Forbidden: {} in {}".format( self.hyperparameter.name, - "{" + ", ".join((repr(value) - for value in sorted(self.values))) + "}") + "{" + ", ".join(repr(value) for value in sorted(self.values)) + "}", + ) - cdef int _is_forbidden(self, value): + def _is_forbidden(self, value) -> int: return value in self.values - cdef int _is_forbidden_vector(self, DTYPE_t value): + def _is_forbidden_vector(self, value) -> int: return value in self.vector_values -cdef class AbstractForbiddenConjunction(AbstractForbiddenComponent): - cdef public tuple components - cdef tuple dlcs - cdef public int n_components - +class AbstractForbiddenConjunction(AbstractForbiddenComponent): def __init__(self, *args: AbstractForbiddenComponent) -> None: - super(AbstractForbiddenConjunction, self).__init__() + super().__init__() # Test the classes for idx, component in enumerate(args): if not isinstance(component, AbstractForbiddenComponent): - raise TypeError("Argument #%d is not an instance of %s, " - "but %s" % ( - idx, AbstractForbiddenComponent, - type(component))) + raise TypeError( + "Argument #%d is not an instance of %s, " + "but %s" % (idx, AbstractForbiddenComponent, type(component)), + ) self.components = args self.n_components = len(self.components) @@ -333,21 +334,19 @@ def __eq__(self, other: Any) -> bool: For __ne__(), by default it delegates to __eq__() and inverts the result unless it is NotImplemented. """ - if not isinstance(other, self.__class__): return False if self.n_components != other.n_components: return False - return all([self.components[i] == other.components[i] - for i in range(self.n_components)]) + return all(self.components[i] == other.components[i] for i in range(self.n_components)) - cpdef set_vector_idx(self, hyperparameter_to_idx): + def set_vector_idx(self, hyperparameter_to_idx) -> None: for component in self.components: component.set_vector_idx(hyperparameter_to_idx) - cpdef get_descendant_literal_clauses(self): + def get_descendant_literal_clauses(self): children = [] for component in self.components: if isinstance(component, AbstractForbiddenConjunction): @@ -356,43 +355,37 @@ def __eq__(self, other: Any) -> bool: children.append(component) return tuple(children) - cpdef is_forbidden(self, instantiated_hyperparameters, strict): + def is_forbidden(self, instantiated_hyperparameters, strict) -> bool: ihp_names = list(instantiated_hyperparameters.keys()) for dlc in self.dlcs: if dlc.hyperparameter.name not in ihp_names: if strict: - raise ValueError("Is_forbidden must be called with all " - "instantiated hyperparameters in the " - "and conjunction of forbidden clauses; " - "you are (at least) missing " - "'%s'" % dlc.hyperparameter.name) + raise ValueError( + "Is_forbidden must be called with all " + "instantiated hyperparameters in the " + "and conjunction of forbidden clauses; " + "you are (at least) missing " + "'%s'" % dlc.hyperparameter.name, + ) else: return False - cdef int* arrptr - arrptr = malloc(sizeof(int) * self.n_components) + values = np.empty(self.n_components, dtype=np.int32) # Finally, call is_forbidden for all direct descendents and combine the # outcomes np_index = 0 for component in self.components: - e = component.is_forbidden(instantiated_hyperparameters, - strict=strict) - arrptr[np_index] = e + e = component.is_forbidden(instantiated_hyperparameters, strict=strict) + values[np_index] = e np_index += 1 - rval = self._is_forbidden(self.n_components, arrptr) - free(arrptr) - return rval + return self._is_forbidden(self.n_components, values) - cdef int c_is_forbidden_vector(self, np.ndarray instantiated_vector, int strict): - cdef int e = 0 - cdef int rval - cdef AbstractForbiddenComponent component - - cdef int* arrptr - arrptr = malloc(sizeof(int) * self.n_components) + def c_is_forbidden_vector(self, instantiated_vector: np.ndarray, strict: int) -> int: + e: int = 0 + values = np.empty(self.n_components, dtype=np.int32) # Finally, call is_forbidden for all direct descendents and combine the # outcomes. Check only as many forbidden clauses as the actual @@ -402,17 +395,15 @@ def __eq__(self, other: Any) -> bool: for i in range(self.n_components): component = self.components[i] e = component.c_is_forbidden_vector(instantiated_vector, strict) - arrptr[i] = e + values[i] = e - rval = self._is_forbidden(self.n_components, arrptr) - free(arrptr) - return rval + return self._is_forbidden(self.n_components, values) - cdef int _is_forbidden(self, int I, int* evaluations): + def _is_forbidden(self, I: int, evaluations) -> int: pass -cdef class ForbiddenAndConjunction(AbstractForbiddenConjunction): +class ForbiddenAndConjunction(AbstractForbiddenConjunction): """A ForbiddenAndConjunction. The ForbiddenAndConjunction combines forbidden-clauses, which allows to @@ -451,7 +442,7 @@ def __repr__(self) -> str: retval.write(")") return retval.getvalue() - cdef int _is_forbidden(self, int I, int* evaluations): + def _is_forbidden(self, I: int, evaluations) -> int: # Return False if one of the components evaluates to False for i in range(I): @@ -459,13 +450,12 @@ def __repr__(self) -> str: return 0 return 1 - cdef int c_is_forbidden_vector(self, np.ndarray instantiated_vector, int strict): + def c_is_forbidden_vector(self, instantiated_vector: np.ndarray, strict: int) -> int: # Copy from above to have early stopping of the evaluation of clauses - # gave only very modest improvements of ~5%; should probably be reworked # if adding more conjunctions in order to use better software design to # avoid code duplication. - cdef int e = 0 - cdef AbstractForbiddenComponent component + e: int = 0 # Finally, call is_forbidden for all direct descendents and combine the # outcomes. Check only as many forbidden clauses as the actual @@ -481,13 +471,8 @@ def __repr__(self) -> str: return 1 -cdef class ForbiddenRelation(AbstractForbiddenComponent): - - cdef public left - cdef public right - cdef public int[2] vector_ids - - def __init__(self, left: Hyperparameter, right : Hyperparameter): +class ForbiddenRelation(AbstractForbiddenComponent): + def __init__(self, left: Hyperparameter, right: Hyperparameter): if not isinstance(left, Hyperparameter): raise TypeError("Argument 'left' is not of type %s." % Hyperparameter) if not isinstance(right, Hyperparameter): @@ -505,70 +490,81 @@ def __eq__(self, other: Any) -> bool: def __copy__(self): return self.__class__( a=copy.copy(self.left), - b=copy.copy(self.right) + b=copy.copy(self.right), ) - cpdef get_descendant_literal_clauses(self): + def get_descendant_literal_clauses(self): return (self,) - cpdef set_vector_idx(self, hyperparameter_to_idx): - self.vector_ids = (hyperparameter_to_idx[self.left.name], hyperparameter_to_idx[self.right.name]) + def set_vector_idx(self, hyperparameter_to_idx): + self.vector_ids = ( + hyperparameter_to_idx[self.left.name], + hyperparameter_to_idx[self.right.name], + ) - cpdef is_forbidden(self, instantiated_hyperparameters, strict): + def is_forbidden(self, instantiated_hyperparameters, strict): left = instantiated_hyperparameters.get(self.left.name) right = instantiated_hyperparameters.get(self.right.name) if left is None: if strict: - raise ValueError("Is_forbidden must be called with the " - "instantiated hyperparameters in the " - "forbidden clause; you are missing " - "'%s'" % self.left.name) + raise ValueError( + "Is_forbidden must be called with the " + "instantiated hyperparameters in the " + "forbidden clause; you are missing " + "'%s'" % self.left.name, + ) else: return False if right is None: if strict: - raise ValueError("Is_forbidden must be called with the " - "instantiated hyperparameters in the " - "forbidden clause; you are missing " - "'%s'" % self.right.name) + raise ValueError( + "Is_forbidden must be called with the " + "instantiated hyperparameters in the " + "forbidden clause; you are missing " + "'%s'" % self.right.name, + ) else: return False return self._is_forbidden(left, right) - cdef int _is_forbidden(self, left, right) except -1: + def _is_forbidden(self, left, right) -> int: pass - cdef int c_is_forbidden_vector(self, np.ndarray instantiated_vector, int strict): - cdef DTYPE_t left = instantiated_vector[self.vector_ids[0]] - cdef DTYPE_t right = instantiated_vector[self.vector_ids[1]] + def c_is_forbidden_vector(self, instantiated_vector: np.ndarray, strict: int) -> int: + left = instantiated_vector[self.vector_ids[0]] + right = instantiated_vector[self.vector_ids[1]] if left != left: if strict: - raise ValueError("Is_forbidden must be called with the " - "instantiated vector id in the " - "forbidden clause; you are missing " - "'%s'" % self.vector_ids[0]) + raise ValueError( + "Is_forbidden must be called with the " + "instantiated vector id in the " + "forbidden clause; you are missing " + "'%s'" % self.vector_ids[0], + ) else: return False if right != right: if strict: - raise ValueError("Is_forbidden must be called with the " - "instantiated vector id in the " - "forbidden clause; you are missing " - "'%s'" % self.vector_ids[1]) + raise ValueError( + "Is_forbidden must be called with the " + "instantiated vector id in the " + "forbidden clause; you are missing " + "'%s'" % self.vector_ids[1], + ) else: return False # Relation is always evaluated against actual value and not vector representation return self._is_forbidden(self.left._transform(left), self.right._transform(right)) - cdef int _is_forbidden_vector(self, DTYPE_t left, DTYPE_t right) except -1: + def _is_forbidden_vector(self, left, right) -> int: pass -cdef class ForbiddenLessThanRelation(ForbiddenRelation): +class ForbiddenLessThanRelation(ForbiddenRelation): """A ForbiddenLessThan relation between two hyperparameters. The ForbiddenLessThan compares the values of two hyperparameters. @@ -597,16 +593,16 @@ def __copy__(self): """ def __repr__(self): - return "Forbidden: %s < %s" % (self.left.name, self.right.name) + return f"Forbidden: {self.left.name} < {self.right.name}" - cdef int _is_forbidden(self, left, right) except -1: + def _is_forbidden(self, left, right) -> int: return left < right - cdef int _is_forbidden_vector(self, DTYPE_t left, DTYPE_t right) except -1: + def _is_forbidden_vector(self, left, right) -> int: return left < right -cdef class ForbiddenEqualsRelation(ForbiddenRelation): +class ForbiddenEqualsRelation(ForbiddenRelation): """A ForbiddenEquals relation between two hyperparameters. The ForbiddenEquals compares the values of two hyperparameters. @@ -634,16 +630,16 @@ def __repr__(self): """ def __repr__(self): - return "Forbidden: %s == %s" % (self.left.name, self.right.name) + return f"Forbidden: {self.left.name} == {self.right.name}" - cdef int _is_forbidden(self, left, right) except -1: + def _is_forbidden(self, left, right) -> int: return left == right - cdef int _is_forbidden_vector(self, DTYPE_t left, DTYPE_t right) except -1: + def _is_forbidden_vector(self, left, right) -> int: return left == right -cdef class ForbiddenGreaterThanRelation(ForbiddenRelation): +class ForbiddenGreaterThanRelation(ForbiddenRelation): """A ForbiddenGreaterThan relation between two hyperparameters. The ForbiddenGreaterThan compares the values of two hyperparameters. @@ -671,10 +667,10 @@ def __repr__(self): """ def __repr__(self): - return "Forbidden: %s > %s" % (self.left.name, self.right.name) + return f"Forbidden: {self.left.name} > {self.right.name}" - cdef int _is_forbidden(self, left, right) except -1: + def _is_forbidden(self, left, right) -> int: return left > right - cdef int _is_forbidden_vector(self, DTYPE_t left, DTYPE_t right) except -1: + def _is_forbidden_vector(self, left, right) -> int: return left > right diff --git a/ConfigSpace/hyperparameters/beta_float.pyx b/ConfigSpace/hyperparameters/beta_float.py similarity index 70% rename from ConfigSpace/hyperparameters/beta_float.pyx rename to ConfigSpace/hyperparameters/beta_float.py index 3192cc2a..36203e22 100644 --- a/ConfigSpace/hyperparameters/beta_float.pyx +++ b/ConfigSpace/hyperparameters/beta_float.py @@ -1,24 +1,29 @@ +from __future__ import annotations + import io import warnings -from typing import Any, Dict, Union, Optional - -from scipy.stats import beta as spbeta +from typing import Any import numpy as np -cimport numpy as np -np.import_array() - -from ConfigSpace.hyperparameters.beta_integer cimport BetaIntegerHyperparameter - - -cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): +from scipy.stats import beta as spbeta - def __init__(self, name: str, alpha: Union[int, float], beta: Union[int, float], - lower: Union[float, int], - upper: Union[float, int], - default_value: Union[None, float] = None, - q: Union[int, float, None] = None, log: bool = False, - meta: Optional[Dict] = None) -> None: +from ConfigSpace.hyperparameters.beta_integer import BetaIntegerHyperparameter +from ConfigSpace.hyperparameters.uniform_float import UniformFloatHyperparameter + + +class BetaFloatHyperparameter(UniformFloatHyperparameter): + def __init__( + self, + name: str, + alpha: int | float, + beta: int | float, + lower: float | int, + upper: float | int, + default_value: None | float = None, + q: int | float | None = None, + log: bool = False, + meta: dict | None = None, + ) -> None: r""" A beta distributed float hyperparameter. The 'lower' and 'upper' parameters move the distribution from the [0, 1]-range and scale it appropriately, but the shape of the @@ -62,24 +67,38 @@ def __init__(self, name: str, alpha: Union[int, float], beta: Union[int, float], # error, which we do by setting default_value = upper - lower / 2 to not raise an error, # then actually call check_default once we have alpha and beta, and are not inside # UniformFloatHP. - super(BetaFloatHyperparameter, self).__init__( - name, lower, upper, (upper + lower) / 2, q, log, meta) + super().__init__( + name, lower, upper, (upper + lower) / 2, q, log, meta, + ) self.alpha = float(alpha) self.beta = float(beta) if (alpha < 1) or (beta < 1): - raise ValueError("Please provide values of alpha and beta larger than or equal to\ - 1 so that the probability density is finite.") + raise ValueError( + "Please provide values of alpha and beta larger than or equal to\ + 1 so that the probability density is finite.", + ) if (self.q is not None) and (self.log is not None) and (default_value is None): - warnings.warn("Logscale and quantization together results in incorrect default values. " - "We recommend specifying a default value manually for this specific case.") + warnings.warn( + "Logscale and quantization together results in incorrect default values. " + "We recommend specifying a default value manually for this specific case.", + ) self.default_value = self.check_default(default_value) self.normalized_default_value = self._inverse_transform(self.default_value) def __repr__(self) -> str: repr_str = io.StringIO() - repr_str.write("%s, Type: BetaFloat, Alpha: %s Beta: %s, Range: [%s, %s], Default: %s" % (self.name, repr(self.alpha), repr(self.beta), repr(self.lower), repr(self.upper), repr(self.default_value))) + repr_str.write( + "{}, Type: BetaFloat, Alpha: {} Beta: {}, Range: [{}, {}], Default: {}".format( + self.name, + repr(self.alpha), + repr(self.beta), + repr(self.lower), + repr(self.upper), + repr(self.default_value), + ), + ) if self.log: repr_str.write(", on log-scale") @@ -105,14 +124,14 @@ def __eq__(self, other: Any) -> bool: return False return ( - self.name == other.name and - self.default_value == other.default_value and - self.alpha == other.alpha and - self.beta == other.beta and - self.log == other.log and - self.q == other.q and - self.lower == other.lower and - self.upper == other.upper + self.name == other.name + and self.default_value == other.default_value + and self.alpha == other.alpha + and self.beta == other.beta + and self.log == other.log + and self.q == other.q + and self.lower == other.lower + and self.upper == other.upper ) def __copy__(self): @@ -125,20 +144,24 @@ def __copy__(self): q=self.q, lower=self.lower, upper=self.upper, - meta=self.meta + meta=self.meta, ) def __hash__(self): return hash((self.name, self.alpha, self.beta, self.lower, self.upper, self.log, self.q)) - def to_uniform(self) -> "UniformFloatHyperparameter": - return UniformFloatHyperparameter(self.name, - self.lower, - self.upper, - default_value=self.default_value, - q=self.q, log=self.log, meta=self.meta) + def to_uniform(self) -> UniformFloatHyperparameter: + return UniformFloatHyperparameter( + self.name, + self.lower, + self.upper, + default_value=self.default_value, + q=self.q, + log=self.log, + meta=self.meta, + ) - def check_default(self, default_value: Union[int, float, None]) -> Union[int, float]: + def check_default(self, default_value: int | float | None) -> int | float: # return mode as default # TODO - for log AND quantization together specifially, this does not give the exact right # value, due to the bounds _lower and _upper being adjusted when quantizing in @@ -160,28 +183,36 @@ def check_default(self, default_value: Union[int, float, None]) -> Union[int, fl else: raise ValueError("Illegal default value %s" % str(default_value)) - def to_integer(self) -> "BetaIntegerHyperparameter": - if self.q is None: - q_int = None - else: - q_int = int(np.rint(self.q)) + def to_integer(self) -> BetaIntegerHyperparameter: + q_int = None if self.q is None else int(np.rint(self.q)) lower = int(np.ceil(self.lower)) upper = int(np.floor(self.upper)) default_value = int(np.rint(self.default_value)) - return BetaIntegerHyperparameter(self.name, lower=lower, upper=upper, alpha=self.alpha, beta=self.beta, - default_value=default_value, q=q_int, log=self.log) + return BetaIntegerHyperparameter( + self.name, + lower=lower, + upper=upper, + alpha=self.alpha, + beta=self.beta, + default_value=default_value, + q=q_int, + log=self.log, + ) - def is_legal(self, value: Union[float]) -> bool: + def is_legal(self, value: float) -> bool: if isinstance(value, (float, int)): return self.upper >= value >= self.lower return False - cpdef bint is_legal_vector(self, DTYPE_t value): + def is_legal_vector(self, value) -> bool: return self._upper >= value >= self._lower - def _sample(self, rs: np.random.RandomState, size: Optional[int] = None - ) -> Union[np.ndarray, float]: + def _sample( + self, + rs: np.random.RandomState, + size: int | None = None, + ) -> np.ndarray | float: alpha = self.alpha beta = self.beta return spbeta(alpha, beta).rvs(size=size, random_state=rs) @@ -201,7 +232,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ @@ -209,8 +240,11 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: lb = self._inverse_transform(self.lower) alpha = self.alpha beta = self.beta - return spbeta(alpha, beta, loc=lb, scale=ub-lb).pdf(vector) \ - * (ub-lb) / (self._upper - self._lower) + return ( + spbeta(alpha, beta, loc=lb, scale=ub - lb).pdf(vector) + * (ub - lb) + / (self._upper - self._lower) + ) def get_max_density(self) -> float: if (self.alpha > 1) or (self.beta > 1): diff --git a/ConfigSpace/hyperparameters/beta_integer.pyx b/ConfigSpace/hyperparameters/beta_integer.py similarity index 69% rename from ConfigSpace/hyperparameters/beta_integer.pyx rename to ConfigSpace/hyperparameters/beta_integer.py index ed31c8aa..bb9172a6 100644 --- a/ConfigSpace/hyperparameters/beta_integer.pyx +++ b/ConfigSpace/hyperparameters/beta_integer.py @@ -1,14 +1,14 @@ -import io -from typing import Any, Dict, Optional, Union +from __future__ import annotations -from scipy.stats import beta as spbeta +import io +from typing import Any import numpy as np -cimport numpy as np -np.import_array() +from scipy.stats import beta as spbeta from ConfigSpace.functional import arange_chunked -from ConfigSpace.hyperparameters.beta_float cimport BetaFloatHyperparameter +from ConfigSpace.hyperparameters import UniformIntegerHyperparameter +from ConfigSpace.hyperparameters.beta_float import BetaFloatHyperparameter # OPTIM: Some operations generate an arange which could blowup memory if # done over the entire space of integers (int32/64). @@ -20,14 +20,19 @@ ARANGE_CHUNKSIZE = 10_000_000 -cdef class BetaIntegerHyperparameter(UniformIntegerHyperparameter): - - def __init__(self, name: str, alpha: Union[int, float], beta: Union[int, float], - lower: Union[int, float], - upper: Union[int, float], - default_value: Union[int, None] = None, q: Union[None, int] = None, - log: bool = False, - meta: Optional[Dict] = None) -> None: +class BetaIntegerHyperparameter(UniformIntegerHyperparameter): + def __init__( + self, + name: str, + alpha: int | float, + beta: int | float, + lower: int | float, + upper: int | float, + default_value: int | None = None, + q: None | int = None, + log: bool = False, + meta: dict | None = None, + ) -> None: r""" A beta distributed integer hyperparameter. The 'lower' and 'upper' parameters move the distribution from the [0, 1]-range and scale it appropriately, but the shape of the @@ -67,25 +72,27 @@ def __init__(self, name: str, alpha: Union[int, float], beta: Union[int, float], Not used by the configuration space. """ - super(BetaIntegerHyperparameter, self).__init__( - name, lower, upper, np.round((upper + lower) / 2), q, log, meta) + super().__init__( + name, lower, upper, np.round((upper + lower) / 2), q, log, meta, + ) self.alpha = float(alpha) self.beta = float(beta) if (alpha < 1) or (beta < 1): - raise ValueError("Please provide values of alpha and beta larger than or equal to\ - 1 so that the probability density is finite.") - if self.q is None: - q = 1 - else: - q = self.q - self.bfhp = BetaFloatHyperparameter(self.name, - self.alpha, - self.beta, - log=self.log, - q=q, - lower=self.lower, - upper=self.upper, - default_value=self.default_value) + raise ValueError( + "Please provide values of alpha and beta larger than or equal to\ + 1 so that the probability density is finite.", + ) + q = 1 if self.q is None else self.q + self.bfhp = BetaFloatHyperparameter( + self.name, + self.alpha, + self.beta, + log=self.log, + q=q, + lower=self.lower, + upper=self.upper, + default_value=self.default_value, + ) self.default_value = self.check_default(default_value) self.normalized_default_value = self._inverse_transform(self.default_value) @@ -93,7 +100,16 @@ def __init__(self, name: str, alpha: Union[int, float], beta: Union[int, float], def __repr__(self) -> str: repr_str = io.StringIO() - repr_str.write("%s, Type: BetaInteger, Alpha: %s Beta: %s, Range: [%s, %s], Default: %s" % (self.name, repr(self.alpha), repr(self.beta), repr(self.lower), repr(self.upper), repr(self.default_value))) + repr_str.write( + "{}, Type: BetaInteger, Alpha: {} Beta: {}, Range: [{}, {}], Default: {}".format( + self.name, + repr(self.alpha), + repr(self.beta), + repr(self.lower), + repr(self.upper), + repr(self.default_value), + ), + ) if self.log: repr_str.write(", on log-scale") @@ -119,13 +135,13 @@ def __eq__(self, other: Any) -> bool: return False return ( - self.name == other.name and - self.alpha == other.alpha and - self.beta == other.beta and - self.log == other.log and - self.q == other.q and - self.lower == other.lower and - self.upper == other.upper + self.name == other.name + and self.alpha == other.alpha + and self.beta == other.beta + and self.log == other.log + and self.q == other.q + and self.lower == other.lower + and self.upper == other.upper ) def __hash__(self): @@ -141,53 +157,56 @@ def __copy__(self): q=self.q, lower=self.lower, upper=self.upper, - meta=self.meta + meta=self.meta, ) - def to_uniform(self) -> "UniformIntegerHyperparameter": - return UniformIntegerHyperparameter(self.name, - self.lower, - self.upper, - default_value=self.default_value, - q=self.q, log=self.log, meta=self.meta) + def to_uniform(self) -> UniformIntegerHyperparameter: + return UniformIntegerHyperparameter( + self.name, + self.lower, + self.upper, + default_value=self.default_value, + q=self.q, + log=self.log, + meta=self.meta, + ) - def check_default(self, default_value: Union[int, float, None]) -> int: + def check_default(self, default_value: int | float | None) -> int: if default_value is None: # Here, we just let the BetaFloat take care of the default value # computation, and just tansform it accordingly value = self.bfhp.check_default(None) value = self._inverse_transform(value) - value = self._transform(value) - return value + return self._transform(value) if self.is_legal(default_value): return default_value - else: - raise ValueError("Illegal default value {}".format(default_value)) - def _sample(self, rs: np.random.RandomState, size: Optional[int] = None - ) -> Union[np.ndarray, float]: + raise ValueError(f"Illegal default value {default_value}") + + def _sample( + self, rs: np.random.RandomState, size: int | None = None, + ) -> np.ndarray | float: value = self.bfhp._sample(rs, size=size) # Map all floats which belong to the same integer value to the same # float value by first transforming it to an integer and then # transforming it back to a float between zero and one value = self._transform(value) - value = self._inverse_transform(value) - return value + return self._inverse_transform(value) def _compute_normalization(self): if self.upper - self.lower > ARANGE_CHUNKSIZE: a = self.bfhp._inverse_transform(self.lower) b = self.bfhp._inverse_transform(self.upper) confidence = 0.999999 - rv = spbeta(self.alpha, self.beta, loc=a, scale=b-a) + rv = spbeta(self.alpha, self.beta, loc=a, scale=b - a) u, v = rv.ppf((1 - confidence) / 2), rv.ppf((1 + confidence) / 2) lb = max(self.bfhp._transform(u), self.lower) ub = min(self.bfhp._transform(v), self.upper + 1) else: lb = self.lower ub = self.upper + 1 - + chunks = arange_chunked(lb, ub, chunk_size=ARANGE_CHUNKSIZE) return sum(self.bfhp.pdf(chunk).sum() for chunk in chunks) @@ -199,7 +218,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: distributions, only normal distributions (as the inverse_transform in the pdf method handles these). Optimally, an IntegerHyperparameter should have a corresponding float, which can be utlized for the calls - to the probability density function (see e.g. NormalIntegerHyperparameter) + to the probability density function (see e.g. NormalIntegerHyperparameter). Parameters ---------- @@ -208,7 +227,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ diff --git a/ConfigSpace/hyperparameters/categorical.pyx b/ConfigSpace/hyperparameters/categorical.py similarity index 91% rename from ConfigSpace/hyperparameters/categorical.pyx rename to ConfigSpace/hyperparameters/categorical.py index 5974819e..8cec9cc1 100644 --- a/ConfigSpace/hyperparameters/categorical.pyx +++ b/ConfigSpace/hyperparameters/categorical.py @@ -4,28 +4,11 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union import numpy as np -cimport numpy as np -np.import_array() -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t +from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter -from ConfigSpace.hyperparameters.hyperparameter cimport Hyperparameter - -cdef class CategoricalHyperparameter(Hyperparameter): - cdef public tuple choices - cdef public tuple weights - cdef public int num_choices - cdef public tuple probabilities - cdef list choices_vector - cdef set _choices_set +class CategoricalHyperparameter(Hyperparameter): # TODO add more magic for automated type recognition # TODO move from list to tuple for choices argument @@ -35,7 +18,7 @@ def __init__( choices: Union[List[Union[str, float, int]], Tuple[Union[float, int, str]]], default_value: Union[int, float, str, None] = None, meta: Optional[Dict] = None, - weights: Optional[Sequence[Union[int, float]]] = None + weights: Optional[Sequence[Union[int, float]]] = None, ) -> None: """ A categorical hyperparameter. @@ -75,7 +58,7 @@ def __init__( raise ValueError( "Choices for categorical hyperparameters %s contain choice '%s' %d " "times, while only a single oocurence is allowed." - % (name, choice, counter[choice]) + % (name, choice, counter[choice]), ) if choice is None: raise TypeError("Choice 'None' is not supported") @@ -174,7 +157,7 @@ def __copy__(self): choices=copy.deepcopy(self.choices), default_value=self.default_value, weights=copy.deepcopy(self.weights), - meta=self.meta + meta=self.meta, ) def to_uniform(self) -> "CategoricalHyperparameter": @@ -192,16 +175,16 @@ def to_uniform(self) -> "CategoricalHyperparameter": name=self.name, choices=copy.deepcopy(self.choices), default_value=self.default_value, - meta=self.meta + meta=self.meta, ) - cpdef int compare(self, value: Union[int, float, str], value2: Union[int, float, str]): + def compare(self, value: Union[int, float, str], value2: Union[int, float, str]) -> int: if value == value2: return 0 else: return 1 - cpdef int compare_vector(self, DTYPE_t value, DTYPE_t value2): + def compare_vector(self, DTYPE_t value, DTYPE_t value2) -> int: if value == value2: return 0 else: @@ -213,7 +196,7 @@ def is_legal(self, value: Union[None, str, float, int]) -> bool: else: return False - cpdef bint is_legal_vector(self, DTYPE_t value): + def is_legal_vector(self, DTYPE_t value) -> int: return value in self._choices_set def _get_probabilities(self, choices: Tuple[Union[None, str, float, int]], @@ -235,7 +218,7 @@ def _get_probabilities(self, choices: Tuple[Union[None, str, float, int]], return tuple(weights / np.sum(weights)) - def check_default(self, default_value: Union[None, str, float, int] + def check_default(self, default_value: Union[None, str, float, int], ) -> Union[str, float, int]: if default_value is None: return self.choices[np.argmax(self.weights) if self.weights is not None else 0] @@ -244,13 +227,13 @@ def check_default(self, default_value: Union[None, str, float, int] else: raise ValueError("Illegal default value %s" % str(default_value)) - def _sample(self, rs: np.random.RandomState, size: Optional[int] = None + def _sample(self, rs: np.random.RandomState, size: Optional[int] = None, ) -> Union[int, np.ndarray]: return rs.choice(a=self.num_choices, size=size, replace=True, p=self.probabilities) - cpdef np.ndarray _transform_vector(self, np.ndarray vector): + def _transform_vector(self, vector: np.ndarray ) -> np.ndarray: if np.isnan(vector).any(): - raise ValueError('Vector %s contains NaN\'s' % vector) + raise ValueError("Vector %s contains NaN\'s" % vector) if np.equal(np.mod(vector, 1), 0): return self.choices[vector.astype(int)] @@ -270,7 +253,7 @@ def _transform_scalar(self, scalar: Union[float, int]) -> Union[float, int, str] "hyperparameter %s with an integer, but provided " "the following float: %f" % (self, scalar)) - def _transform(self, vector: Union[np.ndarray, float, int, str] + def _transform(self, vector: Union[np.ndarray, float, int, str], ) -> Optional[Union[np.ndarray, float, int]]: try: if isinstance(vector, np.ndarray): @@ -291,7 +274,7 @@ def get_num_neighbors(self, value = None) -> int: return len(self.choices) - 1 def get_neighbors(self, value: int, rs: np.random.RandomState, - number: Union[int, float] = np.inf, transform: bool = False + number: Union[int, float] = np.inf, transform: bool = False, ) -> List[Union[float, int, str]]: neighbors = [] # type: List[Union[float, int, str]] if number < len(self.choices): diff --git a/ConfigSpace/hyperparameters/constant.pyx b/ConfigSpace/hyperparameters/constant.py similarity index 64% rename from ConfigSpace/hyperparameters/constant.pyx rename to ConfigSpace/hyperparameters/constant.py index 18c32c5d..f55a85ba 100644 --- a/ConfigSpace/hyperparameters/constant.pyx +++ b/ConfigSpace/hyperparameters/constant.py @@ -1,27 +1,19 @@ -from typing import Any, Dict, List, Optional, Union +from __future__ import annotations -import numpy as np -cimport numpy as np -np.import_array() - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t +from typing import Any, Optional, Union -from ConfigSpace.hyperparameters.hyperparameter cimport Hyperparameter +import numpy as np +from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter -cdef class Constant(Hyperparameter): - cdef public value - cdef DTYPE_t value_vector - def __init__(self, name: str, value: Union[str, int, float], meta: Optional[Dict] = None - ) -> None: +class Constant(Hyperparameter): + def __init__( + self, + name: str, + value: Union[str, int, float], + meta: Optional[dict] = None, + ) -> None: """ Representing a constant hyperparameter in the configuration space. @@ -41,21 +33,19 @@ def __init__(self, name: str, value: Union[str, int, float], meta: Optional[Dict super(Constant, self).__init__(name, meta) allowed_types = (int, float, str) - if not isinstance(value, allowed_types) or \ - isinstance(value, bool): - raise TypeError("Constant value is of type %s, but only the " - "following types are allowed: %s" % - (type(value), allowed_types)) # type: ignore + if not isinstance(value, allowed_types) or isinstance(value, bool): + raise TypeError( + "Constant value is of type %s, but only the " + "following types are allowed: %s" % (type(value), allowed_types), + ) # type: ignore self.value = value - self.value_vector = 0. + self.value_vector = 0.0 self.default_value = value - self.normalized_default_value = 0. + self.normalized_default_value = 0.0 def __repr__(self) -> str: - repr_str = ["%s" % self.name, - "Type: Constant", - "Value: %s" % self.value] + repr_str = ["%s" % self.name, "Type: Constant", "Value: %s" % self.value] return ", ".join(repr_str) def __eq__(self, other: Any) -> bool: @@ -75,9 +65,9 @@ def __eq__(self, other: Any) -> bool: return False return ( - self.value == other.value and - self.name == other.name and - self.default_value == other.default_value + self.value == other.value + and self.name == other.name + and self.default_value == other.default_value ) def __copy__(self): @@ -89,26 +79,31 @@ def __hash__(self): def is_legal(self, value: Union[str, int, float]) -> bool: return value == self.value - cpdef bint is_legal_vector(self, DTYPE_t value): + def is_legal_vector(self, value) -> int: return value == self.value_vector def _sample(self, rs: None, size: Optional[int] = None) -> Union[int, np.ndarray]: return 0 if size == 1 else np.zeros((size,)) - def _transform(self, vector: Optional[Union[np.ndarray, float, int]]) \ - -> Optional[Union[np.ndarray, float, int]]: + def _transform( + self, vector: Optional[Union[np.ndarray, float, int]], + ) -> Optional[Union[np.ndarray, float, int]]: return self.value - def _transform_vector(self, vector: Optional[np.ndarray]) \ - -> Optional[Union[np.ndarray, float, int]]: + def _transform_vector( + self, vector: Optional[np.ndarray], + ) -> Optional[Union[np.ndarray, float, int]]: return self.value - def _transform_scalar(self, vector: Optional[Union[float, int]]) \ - -> Optional[Union[np.ndarray, float, int]]: + def _transform_scalar( + self, vector: Optional[Union[float, int]], + ) -> Optional[Union[np.ndarray, float, int]]: return self.value - def _inverse_transform(self, vector: Union[np.ndarray, float, int] - ) -> Union[np.ndarray, int, float]: + def _inverse_transform( + self, + vector: Union[np.ndarray, float, int], + ) -> Union[np.ndarray, int, float]: if vector != self.value: return np.NaN return 0 @@ -116,11 +111,12 @@ def _inverse_transform(self, vector: Union[np.ndarray, float, int] def has_neighbors(self) -> bool: return False - def get_num_neighbors(self, value = None) -> int: + def get_num_neighbors(self, value=None) -> int: return 0 - def get_neighbors(self, value: Any, rs: np.random.RandomState, number: int, - transform: bool = False) -> List: + def get_neighbors( + self, value: Any, rs: np.random.RandomState, number: int, transform: bool = False, + ) -> list: return [] def pdf(self, vector: np.ndarray) -> np.ndarray: @@ -139,7 +135,7 @@ def pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ @@ -162,7 +158,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ @@ -174,5 +170,6 @@ def get_max_density(self): def get_size(self) -> float: return 1.0 -cdef class UnParametrizedHyperparameter(Constant): + +class UnParametrizedHyperparameter(Constant): pass diff --git a/ConfigSpace/hyperparameters/float_hyperparameter.pyx b/ConfigSpace/hyperparameters/float_hyperparameter.py similarity index 78% rename from ConfigSpace/hyperparameters/float_hyperparameter.pyx rename to ConfigSpace/hyperparameters/float_hyperparameter.py index 06b30dcf..7e709056 100644 --- a/ConfigSpace/hyperparameters/float_hyperparameter.pyx +++ b/ConfigSpace/hyperparameters/float_hyperparameter.py @@ -1,26 +1,34 @@ -from typing import Dict, Optional, Union +from __future__ import annotations + +from typing import Optional, Union import numpy as np -cimport numpy as np -np.import_array() + +from ConfigSpace.hyperparameters.numerical import NumericalHyperparameter -cdef class FloatHyperparameter(NumericalHyperparameter): - def __init__(self, name: str, default_value: Union[int, float], meta: Optional[Dict] = None - ) -> None: +class FloatHyperparameter(NumericalHyperparameter): + def __init__( + self, + name: str, + default_value: Union[int, float], + meta: Optional[dict] = None, + ) -> None: super(FloatHyperparameter, self).__init__(name, default_value, meta) def is_legal(self, value: Union[int, float]) -> bool: raise NotImplementedError() - cpdef bint is_legal_vector(self, DTYPE_t value): + def is_legal_vector(self, value) -> int: raise NotImplementedError() def check_default(self, default_value: Union[int, float]) -> float: raise NotImplementedError() - def _transform(self, vector: Union[np.ndarray, float, int] - ) -> Optional[Union[np.ndarray, float, int]]: + def _transform( + self, + vector: Union[np.ndarray, float, int], + ) -> Optional[Union[np.ndarray, float, int]]: try: if isinstance(vector, np.ndarray): return self._transform_vector(vector) @@ -28,10 +36,10 @@ def _transform(self, vector: Union[np.ndarray, float, int] except ValueError: return None - cpdef double _transform_scalar(self, double scalar): + def _transform_scalar(self, scalar: float) -> float: raise NotImplementedError() - cpdef np.ndarray _transform_vector(self, np.ndarray vector): + def _transform_vector(self, vector: np.ndarray) -> np.ndarray: raise NotImplementedError() def pdf(self, vector: np.ndarray) -> np.ndarray: @@ -50,7 +58,7 @@ def pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ @@ -74,7 +82,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ diff --git a/ConfigSpace/hyperparameters/hyperparameter.pyx b/ConfigSpace/hyperparameters/hyperparameter.py similarity index 94% rename from ConfigSpace/hyperparameters/hyperparameter.pyx rename to ConfigSpace/hyperparameters/hyperparameter.py index e569fe80..0e935f50 100644 --- a/ConfigSpace/hyperparameters/hyperparameter.pyx +++ b/ConfigSpace/hyperparameters/hyperparameter.py @@ -1,11 +1,9 @@ from typing import Dict, Optional, Union import numpy as np -cimport numpy as np -np.import_array() -cdef class Hyperparameter(object): +class Hyperparameter: def __init__(self, name: str, meta: Optional[Dict]) -> None: if not isinstance(name, str): @@ -21,7 +19,7 @@ def __repr__(self): def is_legal(self, value): raise NotImplementedError() - cpdef bint is_legal_vector(self, DTYPE_t value): + def is_legal_vector(self, value) -> int: """ Check whether the given value is a legal value for the vector representation of this hyperparameter. @@ -46,7 +44,7 @@ def sample(self, rs): def rvs( self, size: Optional[int] = None, - random_state: Optional[Union[int, np.random, np.random.RandomState]] = None + random_state: Optional[Union[int, np.random, np.random.RandomState]] = None, ) -> Union[float, np.ndarray]: """ scipy compatibility wrapper for ``_sample``, @@ -86,7 +84,7 @@ def check_random_state(seed): vector = self._sample( rs=check_random_state(random_state), - size=size if size is not None else 1 + size=size if size is not None else 1, ) if size is None: vector = vector[0] @@ -98,7 +96,7 @@ def _sample(self, rs, size): def _transform( self, - vector: Union[np.ndarray, float, int] + vector: Union[np.ndarray, float, int], ) -> Optional[Union[np.ndarray, float, int]]: raise NotImplementedError() @@ -114,7 +112,7 @@ def get_neighbors(self, value, rs, number, transform = False): def get_num_neighbors(self, value): raise NotImplementedError() - cpdef int compare_vector(self, DTYPE_t value, DTYPE_t value2): + def compare_vector(self, DTYPE_t value, DTYPE_t value2) -> int: raise NotImplementedError() def pdf(self, vector: np.ndarray) -> np.ndarray: diff --git a/ConfigSpace/hyperparameters/integer_hyperparameter.pyx b/ConfigSpace/hyperparameters/integer_hyperparameter.py similarity index 73% rename from ConfigSpace/hyperparameters/integer_hyperparameter.pyx rename to ConfigSpace/hyperparameters/integer_hyperparameter.py index 3777c782..87a5f6e3 100644 --- a/ConfigSpace/hyperparameters/integer_hyperparameter.pyx +++ b/ConfigSpace/hyperparameters/integer_hyperparameter.py @@ -1,33 +1,38 @@ -from typing import Dict, Optional, Union +from __future__ import annotations + +from typing import Optional, Union import numpy as np -cimport numpy as np -np.import_array() + +from ConfigSpace.hyperparameters.numerical import NumericalHyperparameter -cdef class IntegerHyperparameter(NumericalHyperparameter): - def __init__(self, name: str, default_value: int, meta: Optional[Dict] = None) -> None: +class IntegerHyperparameter(NumericalHyperparameter): + def __init__(self, name: str, default_value: int, meta: Optional[dict] = None) -> None: super(IntegerHyperparameter, self).__init__(name, default_value, meta) def is_legal(self, value: int) -> bool: - raise NotImplemented + raise NotImplementedError - cpdef bint is_legal_vector(self, DTYPE_t value): - raise NotImplemented + def is_legal_vector(self, value) -> int: + raise NotImplementedError def check_default(self, default_value) -> int: - raise NotImplemented + raise NotImplementedError def check_int(self, parameter: int, name: str) -> int: - if abs(int(parameter) - parameter) > 0.00000001 and \ - type(parameter) is not int: - raise ValueError("For the Integer parameter %s, the value must be " - "an Integer, too. Right now it is a %s with value" - " %s." % (name, type(parameter), str(parameter))) + if abs(int(parameter) - parameter) > 0.00000001 and type(parameter) is not int: + raise ValueError( + "For the Integer parameter %s, the value must be " + "an Integer, too. Right now it is a %s with value" + " %s." % (name, type(parameter), str(parameter)), + ) return int(parameter) - def _transform(self, vector: Union[np.ndarray, float, int] - ) -> Optional[Union[np.ndarray, float, int]]: + def _transform( + self, + vector: Union[np.ndarray, float, int], + ) -> Optional[Union[np.ndarray, float, int]]: try: if isinstance(vector, np.ndarray): return self._transform_vector(vector) @@ -35,10 +40,10 @@ def _transform(self, vector: Union[np.ndarray, float, int] except ValueError: return None - cpdef long long _transform_scalar(self, double scalar): + def _transform_scalar(self, scalar: float) -> float: raise NotImplementedError() - cpdef np.ndarray _transform_vector(self, np.ndarray vector): + def _transform_vector(self, vector: np.ndarray) -> np.ndarray: raise NotImplementedError() def pdf(self, vector: np.ndarray) -> np.ndarray: @@ -57,7 +62,7 @@ def pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ @@ -84,7 +89,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ diff --git a/ConfigSpace/hyperparameters/normal_float.pyx b/ConfigSpace/hyperparameters/normal_float.py similarity index 66% rename from ConfigSpace/hyperparameters/normal_float.pyx rename to ConfigSpace/hyperparameters/normal_float.py index b7716702..f8dcfc40 100644 --- a/ConfigSpace/hyperparameters/normal_float.pyx +++ b/ConfigSpace/hyperparameters/normal_float.py @@ -1,24 +1,30 @@ +from __future__ import annotations + import io import math -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union -from scipy.stats import truncnorm, norm import numpy as np -cimport numpy as np -np.import_array() - -from ConfigSpace.hyperparameters.uniform_float cimport UniformFloatHyperparameter -from ConfigSpace.hyperparameters.normal_integer cimport NormalIntegerHyperparameter - - -cdef class NormalFloatHyperparameter(FloatHyperparameter): - - def __init__(self, name: str, mu: Union[int, float], sigma: Union[int, float], - default_value: Union[None, float] = None, - q: Union[int, float, None] = None, log: bool = False, - lower: Optional[Union[float, int]] = None, - upper: Optional[Union[float, int]] = None, - meta: Optional[Dict] = None) -> None: +from scipy.stats import norm, truncnorm + +from ConfigSpace.hyperparameters.float_hyperparameter import FloatHyperparameter +from ConfigSpace.hyperparameters.normal_integer import NormalIntegerHyperparameter +from ConfigSpace.hyperparameters.uniform_float import UniformFloatHyperparameter + + +class NormalFloatHyperparameter(FloatHyperparameter): + def __init__( + self, + name: str, + mu: Union[int, float], + sigma: Union[int, float], + default_value: Union[None, float] = None, + q: Union[int, float, None] = None, + log: bool = False, + lower: Optional[Union[float, int]] = None, + upper: Optional[Union[float, int]] = None, + meta: Optional[dict] = None, + ) -> None: r""" A normally distributed float hyperparameter. @@ -62,27 +68,31 @@ def __init__(self, name: str, mu: Union[int, float], sigma: Union[int, float], self.normalized_default_value = self._inverse_transform(self.default_value) if (lower is not None) ^ (upper is not None): - raise ValueError("Only one bound was provided when both lower and upper bounds must be provided.") + raise ValueError( + "Only one bound was provided when both lower and upper bounds must be provided.", + ) if lower is not None and upper is not None: self.lower = float(lower) self.upper = float(upper) if self.lower >= self.upper: - raise ValueError("Upper bound %f must be larger than lower bound " - "%f for hyperparameter %s" % - (self.upper, self.lower, name)) + raise ValueError( + "Upper bound %f must be larger than lower bound " + "%f for hyperparameter %s" % (self.upper, self.lower, name), + ) elif log and self.lower <= 0: - raise ValueError("Negative lower bound (%f) for log-scale " - "hyperparameter %s is forbidden." % - (self.lower, name)) + raise ValueError( + "Negative lower bound (%f) for log-scale " + "hyperparameter %s is forbidden." % (self.lower, name), + ) self.default_value = self.check_default(default_value) if self.log: if self.q is not None: - lower = self.lower - (np.float64(self.q) / 2. - 0.0001) - upper = self.upper + (np.float64(self.q) / 2. - 0.0001) + lower = self.lower - (np.float64(self.q) / 2.0 - 0.0001) + upper = self.upper + (np.float64(self.q) / 2.0 - 0.0001) else: lower = self.lower upper = self.upper @@ -90,8 +100,8 @@ def __init__(self, name: str, mu: Union[int, float], sigma: Union[int, float], self._upper = np.log(upper) else: if self.q is not None: - self._lower = self.lower - (self.q / 2. - 0.0001) - self._upper = self.upper + (self.q / 2. - 0.0001) + self._lower = self.lower - (self.q / 2.0 - 0.0001) + self._upper = self.upper + (self.q / 2.0 - 0.0001) else: self._lower = self.lower self._upper = self.upper @@ -102,16 +112,29 @@ def __init__(self, name: str, mu: Union[int, float], sigma: Union[int, float], if np.round((self.upper - self.lower) % self.q, 10) not in (0, self.q): raise ValueError( "Upper bound (%f) - lower bound (%f) must be a multiple of q (%f)" - % (self.upper, self.lower, self.q) + % (self.upper, self.lower, self.q), ) def __repr__(self) -> str: repr_str = io.StringIO() if self.lower is None or self.upper is None: - repr_str.write("%s, Type: NormalFloat, Mu: %s Sigma: %s, Default: %s" % (self.name, repr(self.mu), repr(self.sigma), repr(self.default_value))) + repr_str.write( + "%s, Type: NormalFloat, Mu: %s Sigma: %s, Default: %s" + % (self.name, repr(self.mu), repr(self.sigma), repr(self.default_value)), + ) else: - repr_str.write("%s, Type: NormalFloat, Mu: %s Sigma: %s, Range: [%s, %s], Default: %s" % (self.name, repr(self.mu), repr(self.sigma), repr(self.lower), repr(self.upper), repr(self.default_value))) + repr_str.write( + "%s, Type: NormalFloat, Mu: %s Sigma: %s, Range: [%s, %s], Default: %s" + % ( + self.name, + repr(self.mu), + repr(self.sigma), + repr(self.lower), + repr(self.upper), + repr(self.default_value), + ), + ) if self.log: repr_str.write(", on log-scale") @@ -137,14 +160,14 @@ def __eq__(self, other: Any) -> bool: return False return ( - self.name == other.name and - self.default_value == other.default_value and - self.mu == other.mu and - self.sigma == other.sigma and - self.log == other.log and - self.q == other.q and - self.lower == other.lower and - self.upper == other.upper + self.name == other.name + and self.default_value == other.default_value + and self.mu == other.mu + and self.sigma == other.sigma + and self.log == other.log + and self.q == other.q + and self.lower == other.lower + and self.upper == other.upper ) def __copy__(self): @@ -157,13 +180,13 @@ def __copy__(self): q=self.q, lower=self.lower, upper=self.upper, - meta=self.meta + meta=self.meta, ) def __hash__(self): return hash((self.name, self.mu, self.sigma, self.log, self.q, self.lower, self.upper)) - def to_uniform(self, z: int = 3) -> "UniformFloatHyperparameter": + def to_uniform(self, z: int = 3) -> UniformFloatHyperparameter: if self.lower is None or self.upper is None: lb = self.mu - (z * self.sigma) ub = self.mu + (z * self.sigma) @@ -171,11 +194,15 @@ def to_uniform(self, z: int = 3) -> "UniformFloatHyperparameter": lb = self.lower ub = self.upper - return UniformFloatHyperparameter(self.name, - lb, - ub, - default_value=self.default_value, - q=self.q, log=self.log, meta=self.meta) + return UniformFloatHyperparameter( + self.name, + lb, + ub, + default_value=self.default_value, + q=self.q, + log=self.log, + meta=self.meta, + ) def check_default(self, default_value: Union[int, float]) -> Union[int, float]: if default_value is None: @@ -189,7 +216,7 @@ def check_default(self, default_value: Union[int, float]) -> Union[int, float]: else: raise ValueError("Illegal default value %s" % str(default_value)) - def to_integer(self) -> "NormalIntegerHyperparameter": + def to_integer(self) -> NormalIntegerHyperparameter: if self.q is None: q_int = None else: @@ -198,25 +225,35 @@ def to_integer(self) -> "NormalIntegerHyperparameter": lower = None upper = None else: - lower=np.ceil(self.lower) - upper=np.floor(self.upper) - - return NormalIntegerHyperparameter(self.name, int(np.rint(self.mu)), self.sigma, - lower=lower, upper=upper, - default_value=int(np.rint(self.default_value)), - q=q_int, log=self.log) - - def is_legal(self, value: Union[float]) -> bool: - return (isinstance(value, (float, int, np.number))) and \ - (self.lower is None or value >= self.lower) and \ - (self.upper is None or value <= self.upper) + lower = np.ceil(self.lower) + upper = np.floor(self.upper) + + return NormalIntegerHyperparameter( + self.name, + int(np.rint(self.mu)), + self.sigma, + lower=lower, + upper=upper, + default_value=int(np.rint(self.default_value)), + q=q_int, + log=self.log, + ) - cpdef bint is_legal_vector(self, DTYPE_t value): - return isinstance(value, float) or isinstance(value, int) + def is_legal(self, value: float) -> bool: + return ( + (isinstance(value, (float, int, np.number))) + and (self.lower is None or value >= self.lower) + and (self.upper is None or value <= self.upper) + ) - def _sample(self, rs: np.random.RandomState, size: Optional[int] = None - ) -> Union[np.ndarray, float]: + def is_legal_vector(self, value) -> int: + return isinstance(value, (float, int)) + def _sample( + self, + rs: np.random.RandomState, + size: Optional[int] = None, + ) -> Union[np.ndarray, float]: if self.lower is None: mu = self.mu sigma = self.sigma @@ -231,16 +268,16 @@ def _sample(self, rs: np.random.RandomState, size: Optional[int] = None return truncnorm.rvs(a, b, loc=mu, scale=sigma, size=size, random_state=rs) - cpdef np.ndarray _transform_vector(self, np.ndarray vector): + def _transform_vector(self, vector: np.ndarray) -> np.ndarray: if np.isnan(vector).any(): - raise ValueError('Vector %s contains NaN\'s' % vector) + raise ValueError("Vector %s contains NaN's" % vector) if self.log: vector = np.exp(vector) if self.q is not None: vector = np.rint(vector / self.q) * self.q return vector - cpdef double _transform_scalar(self, double scalar): + def _transform_scalar(self, scalar: float) -> float: if scalar != scalar: raise ValueError("Number %s is NaN" % scalar) if self.log: @@ -249,16 +286,21 @@ def _sample(self, rs: np.random.RandomState, size: Optional[int] = None scalar = np.round(scalar / self.q) * self.q return scalar - def _inverse_transform(self, vector: Optional[np.ndarray]) -> Union[float, np.ndarray]: + def _inverse_transform( + self, vector: Union[float, np.ndarray, None], + ) -> Union[float, np.ndarray]: + # TODO: Should probably use generics here if vector is None: return np.NaN if self.log: vector = np.log(vector) + return vector - def get_neighbors(self, value: float, rs: np.random.RandomState, number: int = 4, - transform: bool = False) -> List[float]: + def get_neighbors( + self, value: float, rs: np.random.RandomState, number: int = 4, transform: bool = False, + ) -> list[float]: neighbors = [] for i in range(number): new_value = rs.normal(value, self.sigma) @@ -292,7 +334,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ diff --git a/ConfigSpace/hyperparameters/normal_integer.pyx b/ConfigSpace/hyperparameters/normal_integer.py similarity index 70% rename from ConfigSpace/hyperparameters/normal_integer.pyx rename to ConfigSpace/hyperparameters/normal_integer.py index ea10d1d1..ea8488ad 100644 --- a/ConfigSpace/hyperparameters/normal_integer.pyx +++ b/ConfigSpace/hyperparameters/normal_integer.py @@ -1,17 +1,18 @@ -from itertools import count +from __future__ import annotations + import io -from more_itertools import roundrobin -from typing import List, Any, Dict, Union, Optional import warnings +from itertools import count +from typing import Any, Optional, Union -from scipy.stats import truncnorm, norm import numpy as np -cimport numpy as np -np.import_array() +from more_itertools import roundrobin +from scipy.stats import norm, truncnorm -from ConfigSpace.functional import center_range, arange_chunked -from ConfigSpace.hyperparameters.uniform_integer cimport UniformIntegerHyperparameter -from ConfigSpace.hyperparameters.normal_float cimport NormalFloatHyperparameter +from ConfigSpace.functional import arange_chunked, center_range +from ConfigSpace.hyperparameters.integer_hyperparameter import IntegerHyperparameter +from ConfigSpace.hyperparameters.normal_float import NormalFloatHyperparameter +from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter # OPTIM: Some operations generate an arange which could blowup memory if # done over the entire space of integers (int32/64). @@ -23,14 +24,19 @@ ARANGE_CHUNKSIZE = 10_000_000 -cdef class NormalIntegerHyperparameter(IntegerHyperparameter): - - def __init__(self, name: str, mu: int, sigma: Union[int, float], - default_value: Union[int, None] = None, q: Union[None, int] = None, - log: bool = False, - lower: Optional[int] = None, - upper: Optional[int] = None, - meta: Optional[Dict] = None) -> None: +class NormalIntegerHyperparameter(IntegerHyperparameter): + def __init__( + self, + name: str, + mu: int, + sigma: Union[int, float], + default_value: Union[int, None] = None, + q: Union[None, int] = None, + log: bool = False, + lower: Optional[int] = None, + upper: Optional[int] = None, + meta: Optional[dict] = None, + ) -> None: r""" A normally distributed integer hyperparameter. @@ -77,9 +83,10 @@ def __init__(self, name: str, mu: int, sigma: Union[int, float], if q is not None: if q < 1: - warnings.warn("Setting quantization < 1 for Integer " - "Hyperparameter '%s' has no effect." % - name) + warnings.warn( + "Setting quantization < 1 for Integer " + "Hyperparameter '%s' has no effect." % name, + ) self.q = None else: self.q = self.check_int(q, "q") @@ -88,30 +95,36 @@ def __init__(self, name: str, mu: int, sigma: Union[int, float], self.log = bool(log) if (lower is not None) ^ (upper is not None): - raise ValueError("Only one bound was provided when both lower and upper bounds must be provided.") + raise ValueError( + "Only one bound was provided when both lower and upper bounds must be provided.", + ) if lower is not None and upper is not None: self.upper = self.check_int(upper, "upper") self.lower = self.check_int(lower, "lower") if self.lower >= self.upper: - raise ValueError("Upper bound %d must be larger than lower bound " - "%d for hyperparameter %s" % - (self.lower, self.upper, name)) + raise ValueError( + "Upper bound %d must be larger than lower bound " + "%d for hyperparameter %s" % (self.lower, self.upper, name), + ) elif log and self.lower <= 0: - raise ValueError("Negative lower bound (%d) for log-scale " - "hyperparameter %s is forbidden." % - (self.lower, name)) + raise ValueError( + "Negative lower bound (%d) for log-scale " + "hyperparameter %s is forbidden." % (self.lower, name), + ) self.lower = lower self.upper = upper - self.nfhp = NormalFloatHyperparameter(self.name, - self.mu, - self.sigma, - log=self.log, - q=self.q, - lower=self.lower, - upper=self.upper, - default_value=default_value) + self.nfhp = NormalFloatHyperparameter( + self.name, + self.mu, + self.sigma, + log=self.log, + q=self.q, + lower=self.lower, + upper=self.upper, + default_value=default_value, + ) self.default_value = self.check_default(default_value) self.normalized_default_value = self._inverse_transform(self.default_value) @@ -126,9 +139,22 @@ def __repr__(self) -> str: repr_str = io.StringIO() if self.lower is None or self.upper is None: - repr_str.write("%s, Type: NormalInteger, Mu: %s Sigma: %s, Default: %s" % (self.name, repr(self.mu), repr(self.sigma), repr(self.default_value))) + repr_str.write( + "%s, Type: NormalInteger, Mu: %s Sigma: %s, Default: %s" + % (self.name, repr(self.mu), repr(self.sigma), repr(self.default_value)), + ) else: - repr_str.write("%s, Type: NormalInteger, Mu: %s Sigma: %s, Range: [%s, %s], Default: %s" % (self.name, repr(self.mu), repr(self.sigma), repr(self.lower), repr(self.upper), repr(self.default_value))) + repr_str.write( + "%s, Type: NormalInteger, Mu: %s Sigma: %s, Range: [%s, %s], Default: %s" + % ( + self.name, + repr(self.mu), + repr(self.sigma), + repr(self.lower), + repr(self.upper), + repr(self.default_value), + ), + ) if self.log: repr_str.write(", on log-scale") @@ -154,14 +180,14 @@ def __eq__(self, other: Any) -> bool: return False return ( - self.name == other.name and - self.mu == other.mu and - self.sigma == other.sigma and - self.log == other.log and - self.q == other.q and - self.lower == other.lower and - self.upper == other.upper and - self.default_value == other.default_value + self.name == other.name + and self.mu == other.mu + and self.sigma == other.sigma + and self.log == other.log + and self.q == other.q + and self.lower == other.lower + and self.upper == other.upper + and self.default_value == other.default_value ) def __hash__(self): @@ -177,10 +203,10 @@ def __copy__(self): q=self.q, lower=self.lower, upper=self.upper, - meta=self.meta + meta=self.meta, ) - def to_uniform(self, z: int = 3) -> "UniformIntegerHyperparameter": + def to_uniform(self, z: int = 3) -> UniformIntegerHyperparameter: if self.lower is None or self.upper is None: lb = np.round(int(self.mu - (z * self.sigma))) ub = np.round(int(self.mu + (z * self.sigma))) @@ -188,19 +214,25 @@ def to_uniform(self, z: int = 3) -> "UniformIntegerHyperparameter": lb = self.lower ub = self.upper - return UniformIntegerHyperparameter(self.name, - lb, - ub, - default_value=self.default_value, - q=self.q, log=self.log, meta=self.meta) + return UniformIntegerHyperparameter( + self.name, + lb, + ub, + default_value=self.default_value, + q=self.q, + log=self.log, + meta=self.meta, + ) def is_legal(self, value: int) -> bool: - return (isinstance(value, (int, np.integer))) and \ - (self.lower is None or value >= self.lower) and \ - (self.upper is None or value <= self.upper) + return ( + (isinstance(value, (int, np.integer))) + and (self.lower is None or value >= self.lower) + and (self.upper is None or value <= self.upper) + ) - cpdef bint is_legal_vector(self, DTYPE_t value): - return isinstance(value, float) or isinstance(value, int) + def is_legal_vector(self, value) -> int: + return isinstance(value, (float, int)) def check_default(self, default_value: int) -> int: if default_value is None: @@ -214,8 +246,11 @@ def check_default(self, default_value: int) -> int: else: raise ValueError("Illegal default value %s" % str(default_value)) - def _sample(self, rs: np.random.RandomState, size: Optional[int] = None - ) -> Union[np.ndarray, float]: + def _sample( + self, + rs: np.random.RandomState, + size: Optional[int] = None, + ) -> Union[np.ndarray, float]: value = self.nfhp._sample(rs, size=size) # Map all floats which belong to the same integer value to the same # float value by first transforming it to an integer and then @@ -224,16 +259,18 @@ def _sample(self, rs: np.random.RandomState, size: Optional[int] = None value = self._inverse_transform(value) return value - cpdef np.ndarray _transform_vector(self, np.ndarray vector): + def _transform_vector(self, vector) -> np.ndarray: vector = self.nfhp._transform_vector(vector) return np.rint(vector) - cpdef long long _transform_scalar(self, double scalar): + def _transform_scalar(self, scalar: float) -> float: scalar = self.nfhp._transform_scalar(scalar) return int(np.round(scalar)) - def _inverse_transform(self, vector: Union[np.ndarray, float, int] - ) -> Union[np.ndarray, float]: + def _inverse_transform( + self, + vector: Union[np.ndarray, float, int], + ) -> Union[np.ndarray, float]: return self.nfhp._inverse_transform(vector) def has_neighbors(self) -> bool: @@ -245,7 +282,7 @@ def get_neighbors( rs: np.random.RandomState, number: int = 4, transform: bool = False, - ) -> List[int]: + ) -> list[int]: stepsize = self.q if self.q is not None else 1 bounded = self.lower is not None mu = self.mu @@ -263,8 +300,8 @@ def get_neighbors( ) else: dist = truncnorm( - a = (self.lower - mu) / sigma, - b = (self.upper - mu) / sigma, + a=(self.lower - mu) / sigma, + b=(self.upper - mu) / sigma, loc=center, scale=sigma, ) @@ -280,24 +317,24 @@ def get_neighbors( # If we already happen to have this neighbor, pick the closest # number around it that is not arelady included if possible_neighbor in neighbors or possible_neighbor == center: - if bounded: - numbers_around = center_range(possible_neighbor, self.lower, self.upper, stepsize) + numbers_around = center_range( + possible_neighbor, self.lower, self.upper, stepsize, + ) else: decrement_count = count(possible_neighbor - stepsize, step=-stepsize) increment_count = count(possible_neighbor + stepsize, step=stepsize) numbers_around = roundrobin(decrement_count, increment_count) valid_numbers_around = ( - n for n in numbers_around - if (n not in neighbors and n != center) + n for n in numbers_around if (n not in neighbors and n != center) ) possible_neighbor = next(valid_numbers_around, None) if possible_neighbor is None: raise ValueError( f"Found no more eligble neighbors for value {center}" - f"\nfound {neighbors}" + f"\nfound {neighbors}", ) # We now have a valid sample, add it to the list of neighbors @@ -310,8 +347,10 @@ def get_neighbors( def _compute_normalization(self): if self.lower is None: - warnings.warn("Cannot normalize the pdf exactly for a NormalIntegerHyperparameter" - f" {self.name} without bounds. Skipping normalization for that hyperparameter.") + warnings.warn( + "Cannot normalize the pdf exactly for a NormalIntegerHyperparameter" + f" {self.name} without bounds. Skipping normalization for that hyperparameter.", + ) return 1 else: @@ -347,7 +386,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ diff --git a/ConfigSpace/hyperparameters/numerical.pyx b/ConfigSpace/hyperparameters/numerical.py similarity index 59% rename from ConfigSpace/hyperparameters/numerical.pyx rename to ConfigSpace/hyperparameters/numerical.py index de8f74c0..b51e9518 100644 --- a/ConfigSpace/hyperparameters/numerical.pyx +++ b/ConfigSpace/hyperparameters/numerical.py @@ -1,24 +1,24 @@ -from typing import Any, Dict, Optional, Union +from __future__ import annotations + +from typing import Any, Optional, Union import numpy as np -cimport numpy as np -np.import_array() +from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter -cdef class NumericalHyperparameter(Hyperparameter): - def __init__(self, name: str, default_value: Any, meta: Optional[Dict]) -> None: - super(NumericalHyperparameter, self).__init__(name, meta) +class NumericalHyperparameter(Hyperparameter): + def __init__(self, name: str, default_value: Any, meta: Optional[dict]) -> None: + super().__init__(name, meta) self.default_value = default_value def has_neighbors(self) -> bool: return True - def get_num_neighbors(self, value = None) -> float: - + def get_num_neighbors(self, value=None) -> float: return np.inf - cpdef int compare(self, value: Union[int, float, str], value2: Union[int, float, str]): + def compare(self, value: Union[int, float, str], value2: Union[int, float, str]) -> int: if value < value2: return -1 elif value > value2: @@ -26,13 +26,14 @@ def get_num_neighbors(self, value = None) -> float: elif value == value2: return 0 - cpdef int compare_vector(self, DTYPE_t value, DTYPE_t value2): + def compare_vector(self, value, value2) -> int: if value < value2: return -1 - elif value > value2: + + if value > value2: return 1 - elif value == value2: - return 0 + + return 0 def allow_greater_less_comparison(self) -> bool: return True @@ -54,24 +55,16 @@ def __eq__(self, other: Any) -> bool: return False return ( - self.name == other.name and - self.default_value == other.default_value and - self.lower == other.lower and - self.upper == other.upper and - self.log == other.log and - self.q == other.q + self.name == other.name + and self.default_value == other.default_value + and self.lower == other.lower + and self.upper == other.upper + and self.log == other.log + and self.q == other.q ) def __hash__(self): - return hash( - ( - self.name, - self.lower, - self.upper, - self.log, - self.q - ) - ) + return hash((self.name, self.lower, self.upper, self.log, self.q)) def __copy__(self): return self.__class__( @@ -81,5 +74,5 @@ def __copy__(self): upper=self.upper, log=self.log, q=self.q, - meta=self.meta + meta=self.meta, ) diff --git a/ConfigSpace/hyperparameters/ordinal.pyx b/ConfigSpace/hyperparameters/ordinal.py similarity index 80% rename from ConfigSpace/hyperparameters/ordinal.pyx rename to ConfigSpace/hyperparameters/ordinal.py index ea90df40..6dbf9c83 100644 --- a/ConfigSpace/hyperparameters/ordinal.pyx +++ b/ConfigSpace/hyperparameters/ordinal.py @@ -1,36 +1,22 @@ -from collections import OrderedDict +from __future__ import annotations + import copy import io -from typing import Any, Dict, List, Optional, Tuple, Union +from collections import OrderedDict +from typing import Any, Optional, Union import numpy as np -cimport numpy as np -np.import_array() - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t - -from ConfigSpace.hyperparameters.hyperparameter cimport Hyperparameter +from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter -cdef class OrdinalHyperparameter(Hyperparameter): - cdef public tuple sequence - cdef public int num_elements - cdef sequence_vector - cdef value_dict +class OrdinalHyperparameter(Hyperparameter): def __init__( self, name: str, - sequence: Union[List[Union[float, int, str]], Tuple[Union[float, int, str]]], + sequence: Union[list[Union[float, int, str]], tuple[Union[float, int, str]]], default_value: Union[str, int, float, None] = None, - meta: Optional[Dict] = None + meta: Optional[dict] = None, ) -> None: """ An ordinal hyperparameter. @@ -59,7 +45,6 @@ def __init__( Field for holding meta data provided by the user. Not used by the configuration space. """ - # Remark # Since the sequence can consist of elements from different types, # they are stored into a dictionary in order to handle them as a @@ -67,7 +52,8 @@ def __init__( super(OrdinalHyperparameter, self).__init__(name, meta) if len(sequence) > len(set(sequence)): raise ValueError( - "Ordinal Hyperparameter Sequence %s contain duplicate values." % sequence) + "Ordinal Hyperparameter Sequence %s contain duplicate values." % sequence, + ) self.sequence = tuple(sequence) self.num_elements = len(sequence) self.sequence_vector = list(range(self.num_elements)) @@ -115,20 +101,20 @@ def __eq__(self, other: Any) -> bool: return False return ( - self.name == other.name and - self.sequence == other.sequence and - self.default_value == other.default_value + self.name == other.name + and self.sequence == other.sequence + and self.default_value == other.default_value ) def __copy__(self): return OrdinalHyperparameter( - name=self.name, - sequence=copy.deepcopy(self.sequence), - default_value=self.default_value, - meta=self.meta - ) + name=self.name, + sequence=copy.deepcopy(self.sequence), + default_value=self.default_value, + meta=self.meta, + ) - cpdef int compare(self, value: Union[int, float, str], value2: Union[int, float, str]): + def compare(self, value: Union[int, float, str], value2: Union[int, float, str]) -> int: if self.value_dict[value] < self.value_dict[value2]: return -1 elif self.value_dict[value] > self.value_dict[value2]: @@ -136,7 +122,7 @@ def __copy__(self): elif self.value_dict[value] == self.value_dict[value2]: return 0 - cpdef int compare_vector(self, DTYPE_t value, DTYPE_t value2): + def compare_vector(self, value, value2) -> int: if value < value2: return -1 elif value > value2: @@ -150,11 +136,13 @@ def is_legal(self, value: Union[int, float, str]) -> bool: """ return value in self.sequence - cpdef bint is_legal_vector(self, DTYPE_t value): + def is_legal_vector(self, value) -> int: return value in self.sequence_vector - def check_default(self, default_value: Optional[Union[int, float, str]] - ) -> Union[int, float, str]: + def check_default( + self, + default_value: Optional[Union[int, float, str]], + ) -> Union[int, float, str]: """ check if given default value is represented in the sequence. If there's no default value we simply choose the @@ -167,16 +155,18 @@ def check_default(self, default_value: Optional[Union[int, float, str]] else: raise ValueError("Illegal default value %s" % str(default_value)) - cpdef np.ndarray _transform_vector(self, np.ndarray vector): + def _transform_vector(self, vector: np.ndarray) -> np.ndarray: if np.isnan(vector).any(): - raise ValueError('Vector %s contains NaN\'s' % vector) + raise ValueError("Vector %s contains NaN's" % vector) if np.equal(np.mod(vector, 1), 0): return self.sequence[vector.astype(int)] - raise ValueError("Can only index the choices of the ordinal " - "hyperparameter %s with an integer, but provided " - "the following float: %f" % (self, vector)) + raise ValueError( + "Can only index the choices of the ordinal " + "hyperparameter %s with an integer, but provided " + "the following float: %f" % (self, vector), + ) def _transform_scalar(self, scalar: Union[float, int]) -> Union[float, int, str]: if scalar != scalar: @@ -185,12 +175,16 @@ def _transform_scalar(self, scalar: Union[float, int]) -> Union[float, int, str] if scalar % 1 == 0: return self.sequence[int(scalar)] - raise ValueError("Can only index the choices of the ordinal " - "hyperparameter %s with an integer, but provided " - "the following float: %f" % (self, scalar)) + raise ValueError( + "Can only index the choices of the ordinal " + "hyperparameter %s with an integer, but provided " + "the following float: %f" % (self, scalar), + ) - def _transform(self, vector: Union[np.ndarray, float, int] - ) -> Optional[Union[np.ndarray, float, int]]: + def _transform( + self, + vector: Union[np.ndarray, float, int], + ) -> Optional[Union[np.ndarray, float, int]]: try: if isinstance(vector, np.ndarray): return self._transform_vector(vector) @@ -198,8 +192,10 @@ def _transform(self, vector: Union[np.ndarray, float, int] except ValueError: return None - def _inverse_transform(self, vector: Optional[Union[np.ndarray, List, int, str, float]] - ) -> Union[float, List[int], List[str], List[float]]: + def _inverse_transform( + self, + vector: Optional[Union[np.ndarray, list, int, str, float]], + ) -> Union[float, list[int], list[str], list[float]]: if vector is None: return np.NaN return self.sequence.index(vector) @@ -260,8 +256,13 @@ def get_num_neighbors(self, value: Union[int, float, str]) -> int: else: return 2 - def get_neighbors(self, value: Union[int, str, float], rs: None, number: int = 0, - transform: bool = False) -> List[Union[str, float, int]]: + def get_neighbors( + self, + value: Union[int, str, float], + rs: None, + number: int = 0, + transform: bool = False, + ) -> list[Union[str, float, int]]: """ Return the neighbors of a given value. Value must be in vector form. Ordinal name will not work. @@ -317,7 +318,7 @@ def pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ @@ -341,12 +342,14 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ if not np.all(np.isin(vector, self.sequence)): - raise ValueError(f"Some element in the vector {vector} is not in the sequence {self.sequence}.") + raise ValueError( + f"Some element in the vector {vector} is not in the sequence {self.sequence}.", + ) return np.ones_like(vector, dtype=np.float64) / self.num_elements def get_max_density(self) -> float: diff --git a/ConfigSpace/hyperparameters/uniform_float.pyx b/ConfigSpace/hyperparameters/uniform_float.py similarity index 74% rename from ConfigSpace/hyperparameters/uniform_float.pyx rename to ConfigSpace/hyperparameters/uniform_float.py index e073662a..c0777a3e 100644 --- a/ConfigSpace/hyperparameters/uniform_float.pyx +++ b/ConfigSpace/hyperparameters/uniform_float.py @@ -1,19 +1,26 @@ +from __future__ import annotations + import io import math -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union import numpy as np -cimport numpy as np -np.import_array() -from ConfigSpace.hyperparameters.uniform_integer cimport UniformIntegerHyperparameter +from ConfigSpace.hyperparameters.float_hyperparameter import FloatHyperparameter +from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter -cdef class UniformFloatHyperparameter(FloatHyperparameter): - def __init__(self, name: str, lower: Union[int, float], upper: Union[int, float], - default_value: Union[int, float, None] = None, - q: Union[int, float, None] = None, log: bool = False, - meta: Optional[Dict] = None) -> None: +class UniformFloatHyperparameter(FloatHyperparameter): + def __init__( + self, + name: str, + lower: Union[int, float], + upper: Union[int, float], + default_value: Union[int, float, None] = None, + q: Union[int, float, None] = None, + log: bool = False, + meta: Optional[dict] = None, + ) -> None: """ A uniformly distributed float hyperparameter. @@ -44,6 +51,7 @@ def __init__(self, name: str, lower: Union[int, float], upper: Union[int, float] Field for holding meta data provided by the user. Not used by the configuration space. """ + default_value = None if default_value is None else float(default_value) super(UniformFloatHyperparameter, self).__init__(name, default_value, meta) self.lower = float(lower) self.upper = float(upper) @@ -51,20 +59,22 @@ def __init__(self, name: str, lower: Union[int, float], upper: Union[int, float] self.log = bool(log) if self.lower >= self.upper: - raise ValueError("Upper bound %f must be larger than lower bound " - "%f for hyperparameter %s" % - (self.upper, self.lower, name)) + raise ValueError( + "Upper bound %f must be larger than lower bound " + "%f for hyperparameter %s" % (self.upper, self.lower, name), + ) elif log and self.lower <= 0: - raise ValueError("Negative lower bound (%f) for log-scale " - "hyperparameter %s is forbidden." % - (self.lower, name)) + raise ValueError( + "Negative lower bound (%f) for log-scale " + "hyperparameter %s is forbidden." % (self.lower, name), + ) self.default_value = self.check_default(default_value) if self.log: if self.q is not None: - lower = self.lower - (np.float64(self.q) / 2. - 0.0001) - upper = self.upper + (np.float64(self.q) / 2. - 0.0001) + lower = self.lower - (np.float64(self.q) / 2.0 - 0.0001) + upper = self.upper + (np.float64(self.q) / 2.0 - 0.0001) else: lower = self.lower upper = self.upper @@ -72,8 +82,8 @@ def __init__(self, name: str, lower: Union[int, float], upper: Union[int, float] self._upper = np.log(upper) else: if self.q is not None: - self._lower = self.lower - (self.q / 2. - 0.0001) - self._upper = self.upper + (self.q / 2. - 0.0001) + self._lower = self.lower - (self.q / 2.0 - 0.0001) + self._upper = self.upper + (self.q / 2.0 - 0.0001) else: self._lower = self.lower self._upper = self.upper @@ -84,16 +94,17 @@ def __init__(self, name: str, lower: Union[int, float], upper: Union[int, float] if np.round((self.upper - self.lower) % self.q, 10) not in (0, self.q): raise ValueError( "Upper bound (%f) - lower bound (%f) must be a multiple of q (%f)" - % (self.upper, self.lower, self.q) + % (self.upper, self.lower, self.q), ) self.normalized_default_value = self._inverse_transform(self.default_value) def __repr__(self) -> str: repr_str = io.StringIO() - repr_str.write("%s, Type: UniformFloat, Range: [%s, %s], Default: %s" % - (self.name, repr(self.lower), repr(self.upper), - repr(self.default_value))) + repr_str.write( + "%s, Type: UniformFloat, Range: [%s, %s], Default: %s" + % (self.name, repr(self.lower), repr(self.upper), repr(self.default_value)), + ) if self.log: repr_str.write(", on log-scale") if self.q is not None: @@ -102,33 +113,33 @@ def __repr__(self) -> str: return repr_str.getvalue() def is_legal(self, value: Union[float]) -> bool: - if not (isinstance(value, float) or isinstance(value, int)): + if not (isinstance(value, (float, int))): return False elif self.upper >= value >= self.lower: return True else: return False - cpdef bint is_legal_vector(self, DTYPE_t value): + def is_legal_vector(self, value) -> bool: if 1.0 >= value >= 0.0: return True else: return False - def check_default(self, default_value: Optional[float]) -> float: + def check_default(self, default_value: Union[float, int, None]) -> float: if default_value is None: if self.log: - default_value = np.exp((np.log(self.lower) + np.log(self.upper)) / 2.) + default_value = float(np.exp((np.log(self.lower) + np.log(self.upper)) / 2.0)) else: - default_value = (self.lower + self.upper) / 2. - default_value = np.round(float(default_value), 10) + default_value = (self.lower + self.upper) / 2.0 + default_value = float(np.round(default_value, 10)) if self.is_legal(default_value): return default_value else: raise ValueError("Illegal default value %s" % str(default_value)) - def to_integer(self) -> "UniformIntegerHyperparameter": + def to_integer(self) -> UniformIntegerHyperparameter: # TODO check if conversion makes sense at all (at least two integer values possible!) # todo check if params should be converted to int while class initialization # or inside class itself @@ -144,9 +155,9 @@ def to_integer(self) -> "UniformIntegerHyperparameter": def _sample(self, rs: np.random, size: Optional[int] = None) -> Union[float, np.ndarray]: return rs.uniform(size=size) - cpdef np.ndarray _transform_vector(self, np.ndarray vector): + def _transform_vector(self, vector: np.ndarray) -> np.ndarray: if np.isnan(vector).any(): - raise ValueError('Vector %s contains NaN\'s' % vector) + raise ValueError("Vector %s contains NaN's" % vector) vector = vector * (self._upper - self._lower) + self._lower if self.log: vector = np.exp(vector) @@ -156,7 +167,7 @@ def _sample(self, rs: np.random, size: Optional[int] = None) -> Union[float, np. vector = np.maximum(vector, self.lower) return np.maximum(self.lower, np.minimum(self.upper, vector)) - cpdef double _transform_scalar(self, double scalar): + def _transform_scalar(self, scalar: float) -> float: if scalar != scalar: raise ValueError("Number %s is NaN" % scalar) scalar = scalar * (self._upper - self._lower) + self._lower @@ -169,8 +180,7 @@ def _sample(self, rs: np.random, size: Optional[int] = None) -> Union[float, np. scalar = min(self.upper, max(self.lower, scalar)) return scalar - def _inverse_transform(self, vector: Union[np.ndarray, None] - ) -> Union[np.ndarray, float, int]: + def _inverse_transform(self, vector: Union[np.ndarray, None]) -> Union[np.ndarray, float, int]: if vector is None: return np.NaN if self.log: @@ -186,8 +196,8 @@ def get_neighbors( rs: np.random.RandomState, number: int = 4, transform: bool = False, - std: float = 0.2 - ) -> List[float]: + std: float = 0.2, + ) -> list[float]: neighbors = [] # type: List[float] while len(neighbors) < number: neighbor = rs.normal(value, std) # type: float @@ -214,7 +224,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ @@ -232,5 +242,5 @@ def get_max_density(self) -> float: def get_size(self) -> float: if self.q is None: return np.inf - else: - return np.rint((self.upper - self.lower) / self.q) + 1 + + return np.rint((self.upper - self.lower) / self.q) + 1 diff --git a/ConfigSpace/hyperparameters/uniform_integer.pyx b/ConfigSpace/hyperparameters/uniform_integer.py similarity index 68% rename from ConfigSpace/hyperparameters/uniform_integer.pyx rename to ConfigSpace/hyperparameters/uniform_integer.py index ce217363..86b8e654 100644 --- a/ConfigSpace/hyperparameters/uniform_integer.pyx +++ b/ConfigSpace/hyperparameters/uniform_integer.py @@ -1,19 +1,26 @@ +from __future__ import annotations + import io -from typing import Dict, List, Optional, Union import warnings import numpy as np -cimport numpy as np -np.import_array() from ConfigSpace.functional import center_range -from ConfigSpace.hyperparameters.uniform_float cimport UniformFloatHyperparameter +from ConfigSpace.hyperparameters.integer_hyperparameter import IntegerHyperparameter +from ConfigSpace.hyperparameters.uniform_float import UniformFloatHyperparameter -cdef class UniformIntegerHyperparameter(IntegerHyperparameter): - def __init__(self, name: str, lower: int, upper: int, default_value: Union[int, None] = None, - q: Union[int, None] = None, log: bool = False, - meta: Optional[Dict] = None) -> None: +class UniformIntegerHyperparameter(IntegerHyperparameter): + def __init__( + self, + name: str, + lower: int, + upper: int, + default_value: int | None = None, + q: int | None = None, + log: bool = False, + meta: dict | None = None, + ) -> None: """ A uniformly distributed integer hyperparameter. @@ -44,54 +51,61 @@ def __init__(self, name: str, lower: int, upper: int, default_value: Union[int, Field for holding meta data provided by the user. Not used by the configuration space. """ - - super(UniformIntegerHyperparameter, self).__init__(name, default_value, meta) self.lower = self.check_int(lower, "lower") self.upper = self.check_int(upper, "upper") + if default_value is not None: default_value = self.check_int(default_value, name) + else: + default_value = self.check_default(default_value) + + # NOTE: Placed after the default value check to ensure it's set and not None + super().__init__(name, default_value, meta) if q is not None: if q < 1: - warnings.warn("Setting quantization < 1 for Integer " - "Hyperparameter '%s' has no effect." % - name) + warnings.warn( + "Setting quantization < 1 for Integer " + "Hyperparameter '%s' has no effect." % name, + ) self.q = None else: self.q = self.check_int(q, "q") if (self.upper - self.lower) % self.q != 0: raise ValueError( "Upper bound (%d) - lower bound (%d) must be a multiple of q (%d)" - % (self.upper, self.lower, self.q) + % (self.upper, self.lower, self.q), ) else: self.q = None self.log = bool(log) if self.lower >= self.upper: - raise ValueError("Upper bound %d must be larger than lower bound " - "%d for hyperparameter %s" % - (self.lower, self.upper, name)) + raise ValueError( + "Upper bound %d must be larger than lower bound " + "%d for hyperparameter %s" % (self.lower, self.upper, name), + ) elif log and self.lower <= 0: - raise ValueError("Negative lower bound (%d) for log-scale " - "hyperparameter %s is forbidden." % - (self.lower, name)) - - self.default_value = self.check_default(default_value) - - self.ufhp = UniformFloatHyperparameter(self.name, - self.lower - 0.49999, - self.upper + 0.49999, - log=self.log, - default_value=self.default_value) + raise ValueError( + "Negative lower bound (%d) for log-scale " + "hyperparameter %s is forbidden." % (self.lower, name), + ) + + self.ufhp = UniformFloatHyperparameter( + self.name, + self.lower - 0.49999, + self.upper + 0.49999, + log=self.log, + default_value=self.default_value, + ) self.normalized_default_value = self._inverse_transform(self.default_value) def __repr__(self) -> str: repr_str = io.StringIO() - repr_str.write("%s, Type: UniformInteger, Range: [%s, %s], Default: %s" - % (self.name, repr(self.lower), - repr(self.upper), repr(self.default_value))) + repr_str.write( + f"{self.name}, Type: UniformInteger, Range: [{self.lower!r}, {self.upper!r}], Default: {self.default_value!r}", + ) if self.log: repr_str.write(", on log-scale") if self.q is not None: @@ -99,17 +113,19 @@ def __repr__(self) -> str: repr_str.seek(0) return repr_str.getvalue() - def _sample(self, rs: np.random.RandomState, size: Optional[int] = None - ) -> Union[np.ndarray, float]: + def _sample( + self, + rs: np.random.RandomState, + size: int | None = None, + ) -> np.ndarray | float: value = self.ufhp._sample(rs, size=size) # Map all floats which belong to the same integer value to the same # float value by first transforming it to an integer and then # transforming it back to a float between zero and one value = self._transform(value) - value = self._inverse_transform(value) - return value + return self._inverse_transform(value) - cpdef np.ndarray _transform_vector(self, np.ndarray vector): + def _transform_vector(self, vector: np.ndarray) -> np.ndarray: vector = self.ufhp._transform_vector(vector) if self.q is not None: vector = np.rint((vector - self.lower) / self.q) * self.q + self.lower @@ -118,7 +134,7 @@ def _sample(self, rs: np.random.RandomState, size: Optional[int] = None return np.rint(vector) - cpdef long long _transform_scalar(self, double scalar): + def _transform_scalar(self, scalar: float) -> float: scalar = self.ufhp._transform_scalar(scalar) if self.q is not None: scalar = np.round((scalar - self.lower) / self.q) * self.q + self.lower @@ -126,30 +142,26 @@ def _sample(self, rs: np.random.RandomState, size: Optional[int] = None scalar = max(scalar, self.lower) return int(np.round(scalar)) - def _inverse_transform(self, vector: Union[np.ndarray, float, int] - ) -> Union[np.ndarray, float, int]: + def _inverse_transform( + self, + vector: np.ndarray | float | int, + ) -> np.ndarray | float | int: return self.ufhp._inverse_transform(vector) def is_legal(self, value: int) -> bool: if not (isinstance(value, (int, np.int32, np.int64))): return False - elif self.upper >= value >= self.lower: - return True - else: - return False + return self.upper >= value >= self.lower - cpdef bint is_legal_vector(self, DTYPE_t value): - if 1.0 >= value >= 0.0: - return True - else: - return False + def is_legal_vector(self, value) -> int: + return 1.0 >= value >= 0.0 - def check_default(self, default_value: Union[int, float]) -> int: + def check_default(self, default_value: int | float | None) -> int: if default_value is None: if self.log: - default_value = np.exp((np.log(self.lower) + np.log(self.upper)) / 2.) + default_value = np.exp((np.log(self.lower) + np.log(self.upper)) / 2.0) else: - default_value = (self.lower + self.upper) / 2. + default_value = (self.lower + self.upper) / 2.0 default_value = int(np.round(default_value, 0)) if self.is_legal(default_value): @@ -166,12 +178,9 @@ def has_neighbors(self) -> bool: lower = self.ufhp._lower # If there is only one active value, this is not enough - if upper - lower >= 1: - return True - else: - return False + return upper - lower >= 1 - def get_num_neighbors(self, value = None) -> int: + def get_num_neighbors(self, value=None) -> int: # If there is a value in the range, then that value is not a neighbor of itself # so we need to remove one if value is not None and self.lower <= value <= self.upper: @@ -186,8 +195,8 @@ def get_neighbors( number: int = 4, transform: bool = False, std: float = 0.2, - ) -> List[int]: - """Get the neighbors of a value + ) -> list[int]: + """Get the neighbors of a value. NOTE ---- @@ -223,22 +232,19 @@ def get_neighbors( " if assumed to be in the unit-hypercube [0, 1]. If this was not" " the behaviour assumed, please raise a ticket on github." ) - assert number < 1000000, ( - "Can only generate less than 1 million neighbors." - ) + assert number < 1000000, "Can only generate less than 1 million neighbors." # Convert python values to cython ones - cdef long long center = self._transform(value) - cdef long long lower = self.lower - cdef long long upper = self.upper - cdef unsigned int n_requested = number - cdef unsigned long long n_neighbors = upper - lower - 1 - cdef long long stepsize = self.q if self.q is not None else 1 + center = self._transform(value) + lower = self.lower + upper = self.upper + n_requested = number + n_neighbors = upper - lower - 1 + stepsize = self.q if self.q is not None else 1 neighbors = [] - cdef long long v # A value that's possible to return + v: int # A value that's possible to return if n_neighbors < n_requested: - for v in range(lower, center): neighbors.append(v) @@ -254,29 +260,24 @@ def get_neighbors( # This will be sampled from and converted to the corresponding int value # However, this is too slow - we use the "poor man's truncnorm below" # cdef np.ndarray float_indices = truncnorm.rvs( - # a=(0 - value) / std, - # b=(1 - value) / std, - # loc=value, - # scale=std, - # size=number, - # random_state=rs - # ) # We sample five times as many values as needed and weed them out below # (perform rejection sampling and make sure we don't sample any neighbor twice) # This increases our chances of not having to fill the neighbors list by calling # `center_range` # Five is an arbitrary number and can probably be tuned to reduce overhead - cdef np.ndarray float_indices = rs.normal(value, std, size=number * 5) - cdef np.ndarray mask = (float_indices >= 0) & (float_indices <= 1) + float_indices: np.ndarray = rs.normal(value, std, size=number * 5) + mask: np.ndarray = (float_indices >= 0) & (float_indices <= 1) float_indices = float_indices[mask] - cdef np.ndarray possible_neighbors_as_array = self._transform_vector(float_indices).astype(np.longlong) - cdef long long [:] possible_neighbors = possible_neighbors_as_array + possible_neighbors_as_array: np.ndarray = self._transform_vector(float_indices).astype( + np.longlong, + ) + possible_neighbors: np.ndarray = possible_neighbors_as_array - cdef unsigned int n_neighbors_generated = 0 - cdef unsigned int n_candidates = len(float_indices) - cdef unsigned int candidate_index = 0 - cdef set seen = {center} + n_neighbors_generated: int = 0 + n_candidates: int = len(float_indices) + candidate_index: int = 0 + seen: set[int] = {center} while n_neighbors_generated < n_requested and candidate_index < n_candidates: v = possible_neighbors[candidate_index] if v not in seen: @@ -308,7 +309,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: distributions, only normal distributions (as the inverse_transform in the pdf method handles these). Optimally, an IntegerHyperparameter should have a corresponding float, which can be utlized for the calls - to the probability density function (see e.g. NormalIntegerHyperparameter) + to the probability density function (see e.g. NormalIntegerHyperparameter). Parameters ---------- @@ -317,7 +318,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ @@ -329,8 +330,5 @@ def get_max_density(self) -> float: return 1 / (ub - lb + 1) def get_size(self) -> float: - if self.q is None: - q = 1 - else: - q = self.q + q = 1 if self.q is None else self.q return np.rint((self.upper - self.lower) / q) + 1 diff --git a/pyproject.toml b/pyproject.toml index 533a6fc9..c5dcfd1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "ConfigSpace" -version = "0.7.2" +version = "0.8.0" description = """\ Creation and manipulation of parameter configuration spaces for \ automated algorithm configuration and hyperparameter tuning. \ """ license.file = "LICENSE" -requires-python = ">=3.7" +requires-python = ">=3.8" readme = "README.md" authors = [ { name = "Matthias Feurer" }, @@ -84,7 +84,7 @@ docs = [ ] [build-system] -requires = ["setuptools", "wheel", "oldest-supported-numpy", "Cython"] +requires = ["setuptools", "wheel", "oldest-supported-numpy", "Cython==0.29.36"] build-backend = "setuptools.build_meta" @@ -113,12 +113,12 @@ exclude_lines = [ ] [tool.black] -target-version = ['py37'] +target-version = ['py38'] line-length = 100 # https://github.com/charliermarsh/ruff [tool.ruff] -target-version = "py37" +target-version = "py38" line-length = 100 show-source = true src = ["ConfigSpace", "test"] @@ -274,7 +274,7 @@ convention = "numpy" [tool.mypy] -python_version = "3.7" +python_version = "3.8" packages = ["ConfigSpace", "test"] show_error_codes = true diff --git a/setup.py b/setup.py index 5eb9bee3..d2360998 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ def finalize_options(self): if PROFILING: COMPILER_DIRECTIVES.update({"profile": True, "linetrace": True}) +""" EXTENSIONS = [ Extension( "ConfigSpace.hyperparameters.beta_float", @@ -109,11 +110,12 @@ def finalize_options(self): Extension("ConfigSpace.conditions", sources=["ConfigSpace/conditions.pyx"]), Extension("ConfigSpace.c_util", sources=["ConfigSpace/c_util.pyx"]), ] +""" setup( name="ConfigSpace", - cmdclass={"build_ext": BuildExt}, - ext_modules=cythonize(EXTENSIONS, compiler_directives=COMPILER_DIRECTIVES), + #cmdclass={"build_ext": BuildExt}, + #ext_modules=cythonize(EXTENSIONS, compiler_directives=COMPILER_DIRECTIVES), packages=find_packages(), ) diff --git a/test/read_and_write/test_json.py b/test/read_and_write/test_json.py index 97703543..725ce45f 100644 --- a/test/read_and_write/test_json.py +++ b/test/read_and_write/test_json.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -import unittest from ConfigSpace import ( Beta, @@ -19,102 +18,107 @@ from ConfigSpace.read_and_write.pcs_new import read as read_pcs_new -class TestJson(unittest.TestCase): - def test_serialize_forbidden_in_clause(self): - cs = ConfigurationSpace() - a = cs.add_hyperparameter(CategoricalHyperparameter("a", [0, 1, 2])) - cs.add_forbidden_clause(ForbiddenInClause(a, [1, 2])) - write(cs) - - def test_serialize_forbidden_relation(self): - cs = ConfigurationSpace() - a = cs.add_hyperparameter(CategoricalHyperparameter("a", [0, 1, 2])) - b = cs.add_hyperparameter(CategoricalHyperparameter("b", [0, 1, 2])) - cs.add_forbidden_clause(ForbiddenLessThanRelation(a, b)) - write(cs) - - def test_configspace_with_probabilities(self): - cs = ConfigurationSpace() - cs.add_hyperparameter(CategoricalHyperparameter("a", [0, 1, 2], weights=[0.2, 0.2, 0.6])) - string = write(cs) - new_cs = read(string) - assert new_cs["a"].probabilities == (0.2, 0.2, 0.6) - - def test_round_trip(self): - this_file = os.path.abspath(__file__) - this_directory = os.path.dirname(this_file) - configuration_space_path = os.path.join(this_directory, "..", "test_searchspaces") - configuration_space_path = os.path.abspath(configuration_space_path) - pcs_files = os.listdir(configuration_space_path) - - for pcs_file in sorted(pcs_files): - if ".pcs" in pcs_file: - full_path = os.path.join(configuration_space_path, pcs_file) - - with open(full_path) as fh: - cs_string = fh.read().split("\n") - try: - cs = read_pcs(cs_string) - except Exception: - cs = read_pcs_new(cs_string) - - cs.name = pcs_file - - json_string = write(cs) - new_cs = read(json_string) - - assert new_cs == cs - - def test_beta_hyperparameter_serialization(self): - # Test for BetaFloatHyperparameter - cs = ConfigurationSpace( - space={ - "p": Float("p", bounds=(0.0, 2.0), q=2, distribution=Beta(1.0, 2.0)), - }, - ) - json_string = write(cs) - new_cs = read(json_string) - assert new_cs == cs - - # Test for BetaIntegerHyperparameter - cs = ConfigurationSpace( - space={ - "p": Integer("p", bounds=(0, 2), q=2, distribution=Beta(1.0, 2.0)), - }, - ) - json_string = write(cs) - new_cs = read(json_string) - assert new_cs == cs - - def test_float_hyperparameter_json_serialization(self): - # Test for NormalFloatHyperparameter - p = Float( - "p", - bounds=(1.0, 9.0), - default=05.0, - q=2, - log=True, - distribution=Normal(1.0, 0.6), - ) - cs1 = ConfigurationSpace(space={"p": p}) - cs2 = read(write(cs1)) - assert cs1 == cs2 - - # Test for UniformFloatHyperparameter - p = Float("p", bounds=(1.0, 9.0), default=2.0, q=2, log=True, distribution=Uniform()) - cs1 = ConfigurationSpace(space={"p": p}) - cs2 = read(write(cs1)) - assert cs1 == cs2 - - def test_integer_hyperparameter_json_serialization(self): - # Test for NormalIntegerHyperparameter - p = Integer("p", bounds=(1, 17), default=2, q=2, log=True, distribution=Normal(1.0, 0.6)) - cs1 = ConfigurationSpace(space={"p": p}) - cs2 = read(write(cs1)) - assert cs1 == cs2 - - # Test for UniformIntegerHyperparameter - p = Integer("p", bounds=(1, 17), default=2, q=2, log=True, distribution=Uniform()) - cs1 = ConfigurationSpace(space={"p": p}) - cs2 = read(write(cs1)) - assert cs1 == cs2 +def test_serialize_forbidden_in_clause(): + cs = ConfigurationSpace() + a = cs.add_hyperparameter(CategoricalHyperparameter("a", [0, 1, 2])) + cs.add_forbidden_clause(ForbiddenInClause(a, [1, 2])) + write(cs) + + +def test_serialize_forbidden_relation(): + cs = ConfigurationSpace() + a = cs.add_hyperparameter(CategoricalHyperparameter("a", [0, 1, 2])) + b = cs.add_hyperparameter(CategoricalHyperparameter("b", [0, 1, 2])) + cs.add_forbidden_clause(ForbiddenLessThanRelation(a, b)) + write(cs) + + +def test_configspace_with_probabilities(): + cs = ConfigurationSpace() + cs.add_hyperparameter(CategoricalHyperparameter("a", [0, 1, 2], weights=[0.2, 0.2, 0.6])) + string = write(cs) + new_cs = read(string) + assert new_cs["a"].probabilities == (0.2, 0.2, 0.6) + + +def test_round_trip(): + this_file = os.path.abspath(__file__) + this_directory = os.path.dirname(this_file) + configuration_space_path = os.path.join(this_directory, "..", "test_searchspaces") + configuration_space_path = os.path.abspath(configuration_space_path) + pcs_files = os.listdir(configuration_space_path) + + for pcs_file in sorted(pcs_files): + if ".pcs" in pcs_file: + full_path = os.path.join(configuration_space_path, pcs_file) + + with open(full_path) as fh: + cs_string = fh.read().split("\n") + try: + cs = read_pcs(cs_string) + except Exception: + cs = read_pcs_new(cs_string) + + cs.name = pcs_file + + json_string = write(cs) + new_cs = read(json_string) + + assert new_cs == cs + + +def test_beta_hyperparameter_serialization(): + # Test for BetaFloatHyperparameter + cs = ConfigurationSpace( + space={ + "p": Float("p", bounds=(0.0, 2.0), q=2, distribution=Beta(1.0, 2.0)), + }, + ) + json_string = write(cs) + new_cs = read(json_string) + assert new_cs == cs + + # Test for BetaIntegerHyperparameter + cs = ConfigurationSpace( + space={ + "p": Integer("p", bounds=(0, 2), q=2, distribution=Beta(1.0, 2.0)), + }, + ) + json_string = write(cs) + new_cs = read(json_string) + assert new_cs == cs + + +def test_float_hyperparameter_json_serialization(): + # Test for NormalFloatHyperparameter + p = Float( + "p", + bounds=(1.0, 9.0), + default=05.0, + q=2, + log=True, + distribution=Normal(1.0, 0.6), + ) + cs1 = ConfigurationSpace(space={"p": p}) + cs2 = read(write(cs1)) + assert cs1 == cs2 + + # Test for UniformFloatHyperparameter + p = Float("p", bounds=(1.0, 9.0), default=2.0, q=2, log=True, distribution=Uniform()) + cs1 = ConfigurationSpace(space={"p": p}) + cs2 = read(write(cs1)) + assert cs1 == cs2 + + +def test_integer_hyperparameter_json_serialization(): + # Test for NormalIntegerHyperparameter + p = Integer("p", bounds=(1, 17), default=2, q=2, log=True, distribution=Normal(1.0, 0.6)) + cs1 = ConfigurationSpace(space={"p": p}) + cs2 = read(write(cs1)) + assert cs1 == cs2 + + # Test for UniformIntegerHyperparameter + p = Integer("p", bounds=(1, 17), default=2, q=2, log=True, distribution=Uniform()) + cs1 = ConfigurationSpace(space={"p": p}) + cs2 = read(write(cs1)) + assert cs1 == cs2 diff --git a/test/read_and_write/test_pcs_converter.py b/test/read_and_write/test_pcs_converter.py index 28e29af5..2077f32d 100644 --- a/test/read_and_write/test_pcs_converter.py +++ b/test/read_and_write/test_pcs_converter.py @@ -29,9 +29,10 @@ import os import tempfile -import unittest from io import StringIO +import pytest + from ConfigSpace.conditions import ( AndConjunction, EqualsCondition, @@ -104,543 +105,551 @@ easy_space.add_hyperparameter(crazy) -class TestPCSConverter(unittest.TestCase): - def setUp(self): - self.maxDiff = None - - def test_read_configuration_space_basic(self): - # TODO: what does this test has to do with the PCS converter? - float_a_copy = UniformFloatHyperparameter("float_a", -1.23, 6.45) - a_copy = {"a": float_a_copy, "b": int_a} - a_real = {"b": int_a, "a": float_a} - self.assertDictEqual(a_real, a_copy) - - """ - Tests for the "older pcs" version - - """ - - def test_read_configuration_space_easy(self): - expected = StringIO() - expected.write("# This is a \n") - expected.write(" # This is a comment with a leading whitespace ### ffds \n") - expected.write("\n") - expected.write("float_a [-1.23, 6.45] [2.61] # bla\n") - expected.write("e_float_a [.5E-2, 4.5e+06] [2250000.0025]\n") - expected.write("int_a [-1, 6] [2]i\n") - expected.write("log_a [4e-1, 6.45] [1.6062378404]l\n") - expected.write("int_log_a [1, 6] [2]il\n") - expected.write('cat_a {a,"b",c,d} [a]\n') - expected.write(r'@.:;/\?!$%&_-<>*+1234567890 {"const"} ["const"]\n') - expected.seek(0) - cs = pcs.read(expected) - assert cs == easy_space - - def test_read_configuration_space_conditional(self): - # More complex search space as string array - complex_cs = [] - complex_cs.append("preprocessing {None, pca} [None]") - complex_cs.append("classifier {svm, nn} [svm]") - complex_cs.append("kernel {rbf, poly, sigmoid} [rbf]") - complex_cs.append("C [0.03125, 32768] [32]l") - complex_cs.append("neurons [16, 1024] [520]i # Should be Q16") - complex_cs.append("lr [0.0001, 1.0] [0.50005]") - complex_cs.append("degree [1, 5] [3]i") - complex_cs.append("gamma [0.000030518, 8] [0.0156251079996]l") - - complex_cs.append("C | classifier in {svm}") - complex_cs.append("kernel | classifier in {svm}") - complex_cs.append("lr | classifier in {nn}") - complex_cs.append("neurons | classifier in {nn}") - complex_cs.append("degree | kernel in {poly, sigmoid}") - complex_cs.append("gamma | kernel in {rbf}") - - cs = pcs.read(complex_cs) - assert cs == conditional_space - - def test_read_configuration_space_conditional_with_two_parents(self): - config_space = [] - config_space.append("@1:0:restarts {F,L,D,x,+,no}[x]") - config_space.append("@1:S:Luby:aryrestarts {1,2}[1]") - config_space.append("@1:2:Luby:restarts [1,65535][1000]il") - config_space.append("@1:2:Luby:restarts | @1:0:restarts in {L}") - config_space.append("@1:2:Luby:restarts | @1:S:Luby:aryrestarts in {2}") - cs = pcs.read(config_space) - assert len(cs.get_conditions()) == 1 - assert isinstance(cs.get_conditions()[0], AndConjunction) - - def test_write_illegal_argument(self): - sp = {"a": int_a} - self.assertRaisesRegex( - TypeError, - r"pcs_parser.write expects an " - r"instance of " - r", you provided " - r"'<(type|class) 'dict'>'", - pcs.write, - sp, - ) - - def test_write_int(self): - expected = "int_a [-1, 6] [2]i" - cs = ConfigurationSpace() - cs.add_hyperparameter(int_a) - value = pcs.write(cs) - assert expected == value - - def test_write_log_int(self): - expected = "int_log_a [1, 6] [2]il" - cs = ConfigurationSpace() - cs.add_hyperparameter(int_log_a) - value = pcs.write(cs) - assert expected == value - - def test_write_q_int(self): - expected = "Q16_int_a [16, 1024] [520]i" - cs = ConfigurationSpace() - cs.add_hyperparameter(UniformIntegerHyperparameter("int_a", 16, 1024, q=16)) - value = pcs.write(cs) - assert expected == value - - def test_write_q_float(self): - expected = "Q16_float_a [16.0, 1024.0] [520.0]" - cs = ConfigurationSpace() - cs.add_hyperparameter(UniformFloatHyperparameter("float_a", 16, 1024, q=16)) - value = pcs.write(cs) - assert expected == value - - def test_write_log10(self): - expected = "a [10.0, 1000.0] [100.0]l" - cs = ConfigurationSpace() - cs.add_hyperparameter(UniformFloatHyperparameter("a", 10, 1000, log=True)) - value = pcs.write(cs) - assert expected == value - - def test_build_forbidden(self): - expected = ( - "a {a, b, c} [a]\nb {a, b, c} [c]\n\n" "{a=a, b=a}\n{a=a, b=b}\n{a=b, b=a}\n{a=b, b=b}" - ) - cs = ConfigurationSpace() - a = CategoricalHyperparameter("a", ["a", "b", "c"], "a") - b = CategoricalHyperparameter("b", ["a", "b", "c"], "c") - cs.add_hyperparameter(a) - cs.add_hyperparameter(b) - fb = ForbiddenAndConjunction( - ForbiddenInClause(a, ["a", "b"]), - ForbiddenInClause(b, ["a", "b"]), - ) - cs.add_forbidden_clause(fb) - value = pcs.write(cs) - assert expected in value - - """ - Tests for the "newer pcs" version in order to check - if both deliver the same results - """ - - def test_read_new_configuration_space_easy(self): - expected = StringIO() - expected.write("# This is a \n") - expected.write(" # This is a comment with a leading whitespace ### ffds \n") - expected.write("\n") - expected.write("float_a real [-1.23, 6.45] [2.61] # bla\n") - expected.write("e_float_a real [.5E-2, 4.5e+06] [2250000.0025]\n") - expected.write("int_a integer [-1, 6] [2]\n") - expected.write("log_a real [4e-1, 6.45] [1.6062378404]log\n") - expected.write("int_log_a integer [1, 6] [2]log\n") - expected.write('cat_a categorical {a,"b",c,d} [a]\n') - expected.write(r'@.:;/\?!$%&_-<>*+1234567890 categorical {"const"} ["const"]\n') - expected.seek(0) - cs = pcs_new.read(expected) - assert cs == easy_space - - def test_read_new_configuration_space_conditional(self): - # More complex search space as string array - complex_cs = [] - complex_cs.append("preprocessing categorical {None, pca} [None]") - complex_cs.append("classifier categorical {svm, nn} [svm]") - complex_cs.append("kernel categorical {rbf, poly, sigmoid} [rbf]") - complex_cs.append("C real [0.03125, 32768] [32]log") - complex_cs.append("neurons integer [16, 1024] [520] # Should be Q16") - complex_cs.append("lr real [0.0001, 1.0] [0.50005]") - complex_cs.append("degree integer [1, 5] [3]") - complex_cs.append("gamma real [0.000030518, 8] [0.0156251079996]log") - - complex_cs.append("C | classifier in {svm}") - complex_cs.append("kernel | classifier in {svm}") - complex_cs.append("lr | classifier in {nn}") - complex_cs.append("neurons | classifier in {nn}") - complex_cs.append("degree | kernel in {poly, sigmoid}") - complex_cs.append("gamma | kernel in {rbf}") - - cs_new = pcs_new.read(complex_cs) - assert cs_new == conditional_space - - # same in older version - complex_cs_old = [] - complex_cs_old.append("preprocessing {None, pca} [None]") - complex_cs_old.append("classifier {svm, nn} [svm]") - complex_cs_old.append("kernel {rbf, poly, sigmoid} [rbf]") - complex_cs_old.append("C [0.03125, 32768] [32]l") - complex_cs_old.append("neurons [16, 1024] [520]i # Should be Q16") - complex_cs_old.append("lr [0.0001, 1.0] [0.50005]") - complex_cs_old.append("degree [1, 5] [3]i") - complex_cs_old.append("gamma [0.000030518, 8] [0.0156251079996]l") - - complex_cs_old.append("C | classifier in {svm}") - complex_cs_old.append("kernel | classifier in {svm}") - complex_cs_old.append("lr | classifier in {nn}") - complex_cs_old.append("neurons | classifier in {nn}") - complex_cs_old.append("degree | kernel in {poly, sigmoid}") - complex_cs_old.append("gamma | kernel in {rbf}") - - cs_old = pcs.read(complex_cs_old) - assert cs_old == cs_new - - def test_write_new_illegal_argument(self): - sp = {"a": int_a} - self.assertRaisesRegex( - TypeError, - r"pcs_parser.write expects an " - r"instance of " - r", you provided " - r"'<(type|class) 'dict'>'", - pcs_new.write, - sp, - ) - - def test_write_new_int(self): - expected = "int_a integer [-1, 6] [2]" - cs = ConfigurationSpace() - cs.add_hyperparameter(int_a) - value = pcs_new.write(cs) - assert expected == value - - def test_write_new_log_int(self): - expected = "int_log_a integer [1, 6] [2]log" - cs = ConfigurationSpace() - cs.add_hyperparameter(int_log_a) - value = pcs_new.write(cs) - assert expected == value - - def test_write_new_q_int(self): - expected = "Q16_int_a integer [16, 1024] [520]" - cs = ConfigurationSpace() - cs.add_hyperparameter(UniformIntegerHyperparameter("int_a", 16, 1024, q=16)) - value = pcs_new.write(cs) - assert expected == value - - def test_write_new_q_float(self): - expected = "Q16_float_a real [16.0, 1024.0] [520.0]" - cs = ConfigurationSpace() - cs.add_hyperparameter(UniformFloatHyperparameter("float_a", 16, 1024, q=16)) - value = pcs_new.write(cs) - assert expected == value - - def test_write_new_log10(self): - expected = "a real [10.0, 1000.0] [100.0]log" - cs = ConfigurationSpace() - cs.add_hyperparameter(UniformFloatHyperparameter("a", 10, 1000, log=True)) - value = pcs_new.write(cs) - assert expected == value - - def test_build_new_forbidden(self): - expected = ( - "a categorical {a, b, c} [a]\nb categorical {a, b, c} [c]\n\n" - "{a=a, b=a}\n{a=a, b=b}\n{a=b, b=a}\n{a=b, b=b}\n" - ) - cs = ConfigurationSpace() - a = CategoricalHyperparameter("a", ["a", "b", "c"], "a") - b = CategoricalHyperparameter("b", ["a", "b", "c"], "c") - cs.add_hyperparameter(a) - cs.add_hyperparameter(b) - fb = ForbiddenAndConjunction( - ForbiddenInClause(a, ["a", "b"]), - ForbiddenInClause(b, ["a", "b"]), - ) - cs.add_forbidden_clause(fb) - value = pcs_new.write(cs) - assert expected == value - - def test_build_new_GreaterThanFloatCondition(self): - expected = "b integer [0, 10] [5]\n" "a real [0.0, 1.0] [0.5]\n\n" "a | b > 5" - cs = ConfigurationSpace() - a = UniformFloatHyperparameter("a", 0, 1, 0.5) - b = UniformIntegerHyperparameter("b", 0, 10, 5) - cs.add_hyperparameter(a) - cs.add_hyperparameter(b) - cond = GreaterThanCondition(a, b, 5) - cs.add_condition(cond) - - value = pcs_new.write(cs) - assert expected == value - - expected = "b real [0.0, 10.0] [5.0]\n" "a real [0.0, 1.0] [0.5]\n\n" "a | b > 5" - cs = ConfigurationSpace() - a = UniformFloatHyperparameter("a", 0, 1, 0.5) - b = UniformFloatHyperparameter("b", 0, 10, 5) - cs.add_hyperparameter(a) - cs.add_hyperparameter(b) - cond = GreaterThanCondition(a, b, 5) - cs.add_condition(cond) - - value = pcs_new.write(cs) - assert expected == value - - def test_build_new_GreaterThanIntCondition(self): - expected = "a real [0.0, 1.0] [0.5]\n" "b integer [0, 10] [5]\n\n" "b | a > 0.5" - cs = ConfigurationSpace() - a = UniformFloatHyperparameter("a", 0, 1, 0.5) - b = UniformIntegerHyperparameter("b", 0, 10, 5) - cs.add_hyperparameter(a) - cs.add_hyperparameter(b) - cond = GreaterThanCondition(b, a, 0.5) - cs.add_condition(cond) - - value = pcs_new.write(cs) - assert expected == value - - expected = "a integer [0, 10] [5]\n" "b integer [0, 10] [5]\n\n" "b | a > 5" - cs = ConfigurationSpace() - a = UniformIntegerHyperparameter("a", 0, 10, 5) - b = UniformIntegerHyperparameter("b", 0, 10, 5) - cs.add_hyperparameter(a) - cs.add_hyperparameter(b) - cond = GreaterThanCondition(b, a, 5) - cs.add_condition(cond) - - value = pcs_new.write(cs) - assert expected == value - - def test_read_new_configuration_space_forbidden(self): - cs_with_forbidden = ConfigurationSpace() - int_hp = UniformIntegerHyperparameter("int_hp", 0, 50, 30) - float_hp = UniformFloatHyperparameter("float_hp", 0.0, 50.0, 30.0) - cat_hp_str = CategoricalHyperparameter("cat_hp_str", ["a", "b", "c"], "b") - ord_hp_str = OrdinalHyperparameter("ord_hp_str", ["a", "b", "c"], "b") - - cs_with_forbidden.add_hyperparameters([int_hp, float_hp, cat_hp_str, ord_hp_str]) - - int_hp_forbidden = ForbiddenAndConjunction(ForbiddenEqualsClause(int_hp, 1)) - - float_hp_forbidden_1 = ForbiddenEqualsClause(float_hp, 1.0) - float_hp_forbidden_2 = ForbiddenEqualsClause(float_hp, 2.0) - float_hp_forbidden = ForbiddenAndConjunction(float_hp_forbidden_1, float_hp_forbidden_2) - - cat_hp_str_forbidden = ForbiddenAndConjunction(ForbiddenEqualsClause(cat_hp_str, "a")) - ord_hp_float_forbidden = ForbiddenAndConjunction(ForbiddenEqualsClause(ord_hp_str, "a")) - cs_with_forbidden.add_forbidden_clauses( - [int_hp_forbidden, float_hp_forbidden, cat_hp_str_forbidden, ord_hp_float_forbidden], - ) - - complex_cs = [] - complex_cs.append("int_hp integer [0,50] [30]") - complex_cs.append("float_hp real [0.0, 50.0] [30.0]") - complex_cs.append("cat_hp_str categorical {a, b, c} [b]") - complex_cs.append("ord_hp_str ordinal {a, b, c} [b]") - complex_cs.append("# Forbiddens:") - complex_cs.append("{int_hp=1}") - complex_cs.append("{float_hp=1.0, float_hp=2.0}") - complex_cs.append("{cat_hp_str=a}") - complex_cs.append("{ord_hp_str=a}") - cs_new = pcs_new.read(complex_cs) - - assert cs_new == cs_with_forbidden - - def test_write_new_configuration_space_forbidden_relation(self): - cs_with_forbidden = ConfigurationSpace() - int_hp = UniformIntegerHyperparameter("int_hp", 0, 50, 30) - float_hp = UniformFloatHyperparameter("float_hp", 0.0, 50.0, 30.0) - - forbidden = ForbiddenGreaterThanRelation(int_hp, float_hp) - cs_with_forbidden.add_hyperparameters([int_hp, float_hp]) - cs_with_forbidden.add_forbidden_clause(forbidden) - - self.assertRaises(TypeError, pcs_new.write, {"configuration_space": cs_with_forbidden}) - - def test_read_new_configuration_space_complex_conditionals(self): - classi = OrdinalHyperparameter( - "classi", - ["random_forest", "extra_trees", "k_nearest_neighbors", "something"], - ) - knn_weights = CategoricalHyperparameter("knn_weights", ["uniform", "distance"]) - weather = OrdinalHyperparameter("weather", ["sunny", "rainy", "cloudy", "snowing"]) - temperature = CategoricalHyperparameter("temperature", ["high", "low"]) - rain = CategoricalHyperparameter("rain", ["yes", "no"]) - gloves = OrdinalHyperparameter("gloves", ["none", "yarn", "leather", "gortex"]) - heur1 = CategoricalHyperparameter("heur1", ["off", "on"]) - heur2 = CategoricalHyperparameter("heur2", ["off", "on"]) - heur_order = CategoricalHyperparameter("heur_order", ["heur1then2", "heur2then1"]) - gloves_condition = OrConjunction( - EqualsCondition(gloves, rain, "yes"), - EqualsCondition(gloves, temperature, "low"), - ) - heur_condition = AndConjunction( - EqualsCondition(heur_order, heur1, "on"), - EqualsCondition(heur_order, heur2, "on"), - ) - and_conjunction = AndConjunction( - NotEqualsCondition(knn_weights, classi, "extra_trees"), - EqualsCondition(knn_weights, classi, "random_forest"), - ) - Cl_condition = OrConjunction( - EqualsCondition(knn_weights, classi, "k_nearest_neighbors"), - and_conjunction, - EqualsCondition(knn_weights, classi, "something"), - ) - - and1 = AndConjunction( - EqualsCondition(temperature, weather, "rainy"), - EqualsCondition(temperature, weather, "cloudy"), - ) - and2 = AndConjunction( - EqualsCondition(temperature, weather, "sunny"), - NotEqualsCondition(temperature, weather, "snowing"), - ) - another_condition = OrConjunction(and1, and2) - - complex_conditional_space = ConfigurationSpace() - complex_conditional_space.add_hyperparameter(classi) - complex_conditional_space.add_hyperparameter(knn_weights) - complex_conditional_space.add_hyperparameter(weather) - complex_conditional_space.add_hyperparameter(temperature) - complex_conditional_space.add_hyperparameter(rain) - complex_conditional_space.add_hyperparameter(gloves) - complex_conditional_space.add_hyperparameter(heur1) - complex_conditional_space.add_hyperparameter(heur2) - complex_conditional_space.add_hyperparameter(heur_order) - - complex_conditional_space.add_condition(gloves_condition) - complex_conditional_space.add_condition(heur_condition) - complex_conditional_space.add_condition(Cl_condition) - complex_conditional_space.add_condition(another_condition) - - complex_cs = [] - complex_cs.append( - "classi ordinal {random_forest,extra_trees,k_nearest_neighbors, something} " - "[random_forest]", - ) - complex_cs.append("knn_weights categorical {uniform, distance} [uniform]") - complex_cs.append("weather ordinal {sunny, rainy, cloudy, snowing} [sunny]") - complex_cs.append("temperature categorical {high, low} [high]") - complex_cs.append("rain categorical { yes, no } [yes]") - complex_cs.append("gloves ordinal { none, yarn, leather, gortex } [none]") - complex_cs.append("heur1 categorical { off, on } [off]") - complex_cs.append("heur2 categorical { off, on } [off]") - complex_cs.append("heur_order categorical { heur1then2, heur2then1 } [heur1then2]") - complex_cs.append("gloves | rain == yes || temperature == low") - complex_cs.append("heur_order | heur1 == on && heur2 == on") - complex_cs.append( - "knn_weights | classi == k_nearest_neighbors || " - "classi != extra_trees && classi == random_forest || classi == something", - ) - complex_cs.append( - "temperature | weather == rainy && weather == cloudy || " - "weather == sunny && weather != snowing", - ) - cs_new = pcs_new.read(complex_cs) - assert cs_new == complex_conditional_space - - def test_convert_restrictions(self): - # This is a smoke test to make sure that the int/float values in the - # greater or smaller statements are converted to the right type when - # reading them - s = """x1 real [0,1] [0] - x2 real [0,1] [0] - x3 real [0,1] [0] - x4 integer [0,2] [0] - x5 real [0,1] [0] - x6 ordinal {cold, luke-warm, hot} [cold] - x1 | x2 > 0.5 - x3 | x4 > 1 && x4 == 2 && x4 in {2} - x5 | x6 > luke-warm""" - - pcs_new.read(s.split("\n")) - - def test_write_restrictions(self): - s = ( - "c integer [0, 2] [0]\n" - + "d ordinal {cold, luke-warm, hot} [cold]\n" - + "e real [0.0, 1.0] [0.0]\n" - + "b real [0.0, 1.0] [0.0]\n" - + "a real [0.0, 1.0] [0.0]\n" - + "\n" - + "b | d in {luke-warm, hot} || c > 1\n" - + "a | b == 0.5 && e > 0.5" - ) - - a = pcs_new.read(s.split("\n")) - out = pcs_new.write(a) - assert out == s - - def test_read_write(self): - # Some smoke tests whether reading, writing, reading alters makes the - # configspace incomparable - this_file = os.path.abspath(__file__) - this_directory = os.path.dirname(this_file) - configuration_space_path = os.path.join(this_directory, "..", "test_searchspaces") - configuration_space_path = os.path.abspath(configuration_space_path) - configuration_space_path = os.path.join(configuration_space_path, "spear-params-mixed.pcs") - with open(configuration_space_path) as fh: - cs = pcs.read(fh) - - tf = tempfile.NamedTemporaryFile() - name = tf.name - tf.close() - with open(name, "w") as fh: - pcs_string = pcs.write(cs) - fh.write(pcs_string) - with open(name) as fh: - pcs_new = pcs.read(fh) - - assert pcs_new == cs, (pcs_new, cs) - - def test_write_categorical_with_weights(self): - cat = CategoricalHyperparameter("a", ["a", "b"], weights=[0.3, 0.7]) - cs = ConfigurationSpace() - cs.add_hyperparameter(cat) - with self.assertRaisesRegex(ValueError, "The pcs format does not support"): - pcs.write(cs) - with self.assertRaisesRegex(ValueError, "The pcs format does not support"): - pcs.write(cs) - - def test_write_numerical_cond_and_forb(self): - cs = ConfigurationSpace(seed=12345) - - hc1 = CategoricalHyperparameter(name="hc1", choices=[True, False], default_value=True) - hc2 = CategoricalHyperparameter(name="hc2", choices=[True, False], default_value=True) - - hf1 = UniformFloatHyperparameter(name="hf1", lower=1.0, upper=10.0, default_value=5.0) - hi1 = UniformIntegerHyperparameter(name="hi1", lower=1, upper=10, default_value=5) - cs.add_hyperparameters([hc1, hc2, hf1, hi1]) - c1 = InCondition(child=hc1, parent=hc2, values=[True]) - c2 = InCondition(child=hc2, parent=hi1, values=[1, 2, 3, 4]) - c3 = GreaterThanCondition(hi1, hf1, 6.0) - c4 = EqualsCondition(hi1, hf1, 8.0) - c5 = AndConjunction(c3, c4) - - cs.add_conditions([c1, c2, c5]) - - f1 = ForbiddenEqualsClause(hc1, False) - f2 = ForbiddenEqualsClause(hf1, 2.0) - f3 = ForbiddenEqualsClause(hi1, 3) - cs.add_forbidden_clauses([ForbiddenAndConjunction(f2, f3), f1]) - expected = ( - "hf1 real [1.0, 10.0] [5.0]\n" - + "hi1 integer [1, 10] [5]\n" - + "hc2 categorical {True, False} [True]\n" - + "hc1 categorical {True, False} [True]\n" - + "\n" - + "hi1 | hf1 > 6.0 && hf1 == 8.0\n" - + "hc2 | hi1 in {1, 2, 3, 4}\n" - + "hc1 | hc2 in {True}\n" - + "\n" - + "{hc1=False}\n" - + "{hf1=2.0, hi1=3}\n" - ) - out = pcs_new.write(cs) - assert out == expected +def test_read_configuration_space_basic(): + # TODO: what does this test has to do with the PCS converter? + float_a_copy = UniformFloatHyperparameter("float_a", -1.23, 6.45) + a_copy = {"a": float_a_copy, "b": int_a} + a_real = {"b": int_a, "a": float_a} + assert a_real == a_copy + + +""" +Tests for the "older pcs" version + +""" + + +def test_read_configuration_space_easy(): + expected = StringIO() + expected.write("# This is a \n") + expected.write(" # This is a comment with a leading whitespace ### ffds \n") + expected.write("\n") + expected.write("float_a [-1.23, 6.45] [2.61] # bla\n") + expected.write("e_float_a [.5E-2, 4.5e+06] [2250000.0025]\n") + expected.write("int_a [-1, 6] [2]i\n") + expected.write("log_a [4e-1, 6.45] [1.6062378404]l\n") + expected.write("int_log_a [1, 6] [2]il\n") + expected.write('cat_a {a,"b",c,d} [a]\n') + expected.write(r'@.:;/\?!$%&_-<>*+1234567890 {"const"} ["const"]\n') + expected.seek(0) + cs = pcs.read(expected) + assert cs == easy_space + + +def test_read_configuration_space_conditional(): + # More complex search space as string array + complex_cs = [] + complex_cs.append("preprocessing {None, pca} [None]") + complex_cs.append("classifier {svm, nn} [svm]") + complex_cs.append("kernel {rbf, poly, sigmoid} [rbf]") + complex_cs.append("C [0.03125, 32768] [32]l") + complex_cs.append("neurons [16, 1024] [520]i # Should be Q16") + complex_cs.append("lr [0.0001, 1.0] [0.50005]") + complex_cs.append("degree [1, 5] [3]i") + complex_cs.append("gamma [0.000030518, 8] [0.0156251079996]l") + + complex_cs.append("C | classifier in {svm}") + complex_cs.append("kernel | classifier in {svm}") + complex_cs.append("lr | classifier in {nn}") + complex_cs.append("neurons | classifier in {nn}") + complex_cs.append("degree | kernel in {poly, sigmoid}") + complex_cs.append("gamma | kernel in {rbf}") + + cs = pcs.read(complex_cs) + assert cs == conditional_space + + +def test_read_configuration_space_conditional_with_two_parents(): + config_space = [] + config_space.append("@1:0:restarts {F,L,D,x,+,no}[x]") + config_space.append("@1:S:Luby:aryrestarts {1,2}[1]") + config_space.append("@1:2:Luby:restarts [1,65535][1000]il") + config_space.append("@1:2:Luby:restarts | @1:0:restarts in {L}") + config_space.append("@1:2:Luby:restarts | @1:S:Luby:aryrestarts in {2}") + cs = pcs.read(config_space) + assert len(cs.get_conditions()) == 1 + assert isinstance(cs.get_conditions()[0], AndConjunction) + + +def test_write_illegal_argument(): + sp = {"a": int_a} + with pytest.raises(TypeError): + pcs.write(sp) + + +def test_write_int(): + expected = "int_a [-1, 6] [2]i" + cs = ConfigurationSpace() + cs.add_hyperparameter(int_a) + value = pcs.write(cs) + assert expected == value + + +def test_write_log_int(): + expected = "int_log_a [1, 6] [2]il" + cs = ConfigurationSpace() + cs.add_hyperparameter(int_log_a) + value = pcs.write(cs) + assert expected == value + + +def test_write_q_int(): + expected = "Q16_int_a [16, 1024] [520]i" + cs = ConfigurationSpace() + cs.add_hyperparameter(UniformIntegerHyperparameter("int_a", 16, 1024, q=16)) + value = pcs.write(cs) + assert expected == value + + +def test_write_q_float(): + expected = "Q16_float_a [16.0, 1024.0] [520.0]" + cs = ConfigurationSpace() + cs.add_hyperparameter(UniformFloatHyperparameter("float_a", 16, 1024, q=16)) + value = pcs.write(cs) + assert expected == value + + +def test_write_log10(): + expected = "a [10.0, 1000.0] [100.0]l" + cs = ConfigurationSpace() + cs.add_hyperparameter(UniformFloatHyperparameter("a", 10, 1000, log=True)) + value = pcs.write(cs) + assert expected == value + + +def test_build_forbidden(): + expected = ( + "a {a, b, c} [a]\nb {a, b, c} [c]\n\n" "{a=a, b=a}\n{a=a, b=b}\n{a=b, b=a}\n{a=b, b=b}" + ) + cs = ConfigurationSpace() + a = CategoricalHyperparameter("a", ["a", "b", "c"], "a") + b = CategoricalHyperparameter("b", ["a", "b", "c"], "c") + cs.add_hyperparameter(a) + cs.add_hyperparameter(b) + fb = ForbiddenAndConjunction( + ForbiddenInClause(a, ["a", "b"]), + ForbiddenInClause(b, ["a", "b"]), + ) + cs.add_forbidden_clause(fb) + value = pcs.write(cs) + assert expected in value + + +""" +Tests for the "newer pcs" version in order to check +if both deliver the same results +""" + + +def test_read_new_configuration_space_easy(): + expected = StringIO() + expected.write("# This is a \n") + expected.write(" # This is a comment with a leading whitespace ### ffds \n") + expected.write("\n") + expected.write("float_a real [-1.23, 6.45] [2.61] # bla\n") + expected.write("e_float_a real [.5E-2, 4.5e+06] [2250000.0025]\n") + expected.write("int_a integer [-1, 6] [2]\n") + expected.write("log_a real [4e-1, 6.45] [1.6062378404]log\n") + expected.write("int_log_a integer [1, 6] [2]log\n") + expected.write('cat_a categorical {a,"b",c,d} [a]\n') + expected.write(r'@.:;/\?!$%&_-<>*+1234567890 categorical {"const"} ["const"]\n') + expected.seek(0) + cs = pcs_new.read(expected) + assert cs == easy_space + + +def test_read_new_configuration_space_conditional(): + # More complex search space as string array + complex_cs = [] + complex_cs.append("preprocessing categorical {None, pca} [None]") + complex_cs.append("classifier categorical {svm, nn} [svm]") + complex_cs.append("kernel categorical {rbf, poly, sigmoid} [rbf]") + complex_cs.append("C real [0.03125, 32768] [32]log") + complex_cs.append("neurons integer [16, 1024] [520] # Should be Q16") + complex_cs.append("lr real [0.0001, 1.0] [0.50005]") + complex_cs.append("degree integer [1, 5] [3]") + complex_cs.append("gamma real [0.000030518, 8] [0.0156251079996]log") + + complex_cs.append("C | classifier in {svm}") + complex_cs.append("kernel | classifier in {svm}") + complex_cs.append("lr | classifier in {nn}") + complex_cs.append("neurons | classifier in {nn}") + complex_cs.append("degree | kernel in {poly, sigmoid}") + complex_cs.append("gamma | kernel in {rbf}") + + cs_new = pcs_new.read(complex_cs) + assert cs_new == conditional_space + + # same in older version + complex_cs_old = [] + complex_cs_old.append("preprocessing {None, pca} [None]") + complex_cs_old.append("classifier {svm, nn} [svm]") + complex_cs_old.append("kernel {rbf, poly, sigmoid} [rbf]") + complex_cs_old.append("C [0.03125, 32768] [32]l") + complex_cs_old.append("neurons [16, 1024] [520]i # Should be Q16") + complex_cs_old.append("lr [0.0001, 1.0] [0.50005]") + complex_cs_old.append("degree [1, 5] [3]i") + complex_cs_old.append("gamma [0.000030518, 8] [0.0156251079996]l") + + complex_cs_old.append("C | classifier in {svm}") + complex_cs_old.append("kernel | classifier in {svm}") + complex_cs_old.append("lr | classifier in {nn}") + complex_cs_old.append("neurons | classifier in {nn}") + complex_cs_old.append("degree | kernel in {poly, sigmoid}") + complex_cs_old.append("gamma | kernel in {rbf}") + + cs_old = pcs.read(complex_cs_old) + assert cs_old == cs_new + + +def test_write_new_illegal_argument(): + sp = {"a": int_a} + with pytest.raises(TypeError): + pcs_new.write(sp) + + +def test_write_new_int(): + expected = "int_a integer [-1, 6] [2]" + cs = ConfigurationSpace() + cs.add_hyperparameter(int_a) + value = pcs_new.write(cs) + assert expected == value + + +def test_write_new_log_int(): + expected = "int_log_a integer [1, 6] [2]log" + cs = ConfigurationSpace() + cs.add_hyperparameter(int_log_a) + value = pcs_new.write(cs) + assert expected == value + + +def test_write_new_q_int(): + expected = "Q16_int_a integer [16, 1024] [520]" + cs = ConfigurationSpace() + cs.add_hyperparameter(UniformIntegerHyperparameter("int_a", 16, 1024, q=16)) + value = pcs_new.write(cs) + assert expected == value + + +def test_write_new_q_float(): + expected = "Q16_float_a real [16.0, 1024.0] [520.0]" + cs = ConfigurationSpace() + cs.add_hyperparameter(UniformFloatHyperparameter("float_a", 16, 1024, q=16)) + value = pcs_new.write(cs) + assert expected == value + + +def test_write_new_log10(): + expected = "a real [10.0, 1000.0] [100.0]log" + cs = ConfigurationSpace() + cs.add_hyperparameter(UniformFloatHyperparameter("a", 10, 1000, log=True)) + value = pcs_new.write(cs) + assert expected == value + + +def test_build_new_forbidden(): + expected = ( + "a categorical {a, b, c} [a]\nb categorical {a, b, c} [c]\n\n" + "{a=a, b=a}\n{a=a, b=b}\n{a=b, b=a}\n{a=b, b=b}\n" + ) + cs = ConfigurationSpace() + a = CategoricalHyperparameter("a", ["a", "b", "c"], "a") + b = CategoricalHyperparameter("b", ["a", "b", "c"], "c") + cs.add_hyperparameter(a) + cs.add_hyperparameter(b) + fb = ForbiddenAndConjunction( + ForbiddenInClause(a, ["a", "b"]), + ForbiddenInClause(b, ["a", "b"]), + ) + cs.add_forbidden_clause(fb) + value = pcs_new.write(cs) + assert expected == value + + +def test_build_new_GreaterThanFloatCondition(): + expected = "b integer [0, 10] [5]\n" "a real [0.0, 1.0] [0.5]\n\n" "a | b > 5" + cs = ConfigurationSpace() + a = UniformFloatHyperparameter("a", 0, 1, 0.5) + b = UniformIntegerHyperparameter("b", 0, 10, 5) + cs.add_hyperparameter(a) + cs.add_hyperparameter(b) + cond = GreaterThanCondition(a, b, 5) + cs.add_condition(cond) + + value = pcs_new.write(cs) + assert expected == value + + expected = "b real [0.0, 10.0] [5.0]\n" "a real [0.0, 1.0] [0.5]\n\n" "a | b > 5" + cs = ConfigurationSpace() + a = UniformFloatHyperparameter("a", 0, 1, 0.5) + b = UniformFloatHyperparameter("b", 0, 10, 5) + cs.add_hyperparameter(a) + cs.add_hyperparameter(b) + cond = GreaterThanCondition(a, b, 5) + cs.add_condition(cond) + + value = pcs_new.write(cs) + assert expected == value + + +def test_build_new_GreaterThanIntCondition(): + expected = "a real [0.0, 1.0] [0.5]\n" "b integer [0, 10] [5]\n\n" "b | a > 0.5" + cs = ConfigurationSpace() + a = UniformFloatHyperparameter("a", 0, 1, 0.5) + b = UniformIntegerHyperparameter("b", 0, 10, 5) + cs.add_hyperparameter(a) + cs.add_hyperparameter(b) + cond = GreaterThanCondition(b, a, 0.5) + cs.add_condition(cond) + + value = pcs_new.write(cs) + assert expected == value + + expected = "a integer [0, 10] [5]\n" "b integer [0, 10] [5]\n\n" "b | a > 5" + cs = ConfigurationSpace() + a = UniformIntegerHyperparameter("a", 0, 10, 5) + b = UniformIntegerHyperparameter("b", 0, 10, 5) + cs.add_hyperparameter(a) + cs.add_hyperparameter(b) + cond = GreaterThanCondition(b, a, 5) + cs.add_condition(cond) + + value = pcs_new.write(cs) + assert expected == value + + +def test_read_new_configuration_space_forbidden(): + cs_with_forbidden = ConfigurationSpace() + int_hp = UniformIntegerHyperparameter("int_hp", 0, 50, 30) + float_hp = UniformFloatHyperparameter("float_hp", 0.0, 50.0, 30.0) + cat_hp_str = CategoricalHyperparameter("cat_hp_str", ["a", "b", "c"], "b") + ord_hp_str = OrdinalHyperparameter("ord_hp_str", ["a", "b", "c"], "b") + + cs_with_forbidden.add_hyperparameters([int_hp, float_hp, cat_hp_str, ord_hp_str]) + + int_hp_forbidden = ForbiddenAndConjunction(ForbiddenEqualsClause(int_hp, 1)) + + float_hp_forbidden_1 = ForbiddenEqualsClause(float_hp, 1.0) + float_hp_forbidden_2 = ForbiddenEqualsClause(float_hp, 2.0) + float_hp_forbidden = ForbiddenAndConjunction(float_hp_forbidden_1, float_hp_forbidden_2) + + cat_hp_str_forbidden = ForbiddenAndConjunction(ForbiddenEqualsClause(cat_hp_str, "a")) + ord_hp_float_forbidden = ForbiddenAndConjunction(ForbiddenEqualsClause(ord_hp_str, "a")) + cs_with_forbidden.add_forbidden_clauses( + [int_hp_forbidden, float_hp_forbidden, cat_hp_str_forbidden, ord_hp_float_forbidden], + ) + + complex_cs = [] + complex_cs.append("int_hp integer [0,50] [30]") + complex_cs.append("float_hp real [0.0, 50.0] [30.0]") + complex_cs.append("cat_hp_str categorical {a, b, c} [b]") + complex_cs.append("ord_hp_str ordinal {a, b, c} [b]") + complex_cs.append("# Forbiddens:") + complex_cs.append("{int_hp=1}") + complex_cs.append("{float_hp=1.0, float_hp=2.0}") + complex_cs.append("{cat_hp_str=a}") + complex_cs.append("{ord_hp_str=a}") + cs_new = pcs_new.read(complex_cs) + + assert cs_new == cs_with_forbidden + + +def test_write_new_configuration_space_forbidden_relation(): + cs_with_forbidden = ConfigurationSpace() + int_hp = UniformIntegerHyperparameter("int_hp", 0, 50, 30) + float_hp = UniformFloatHyperparameter("float_hp", 0.0, 50.0, 30.0) + + forbidden = ForbiddenGreaterThanRelation(int_hp, float_hp) + cs_with_forbidden.add_hyperparameters([int_hp, float_hp]) + cs_with_forbidden.add_forbidden_clause(forbidden) + + with pytest.raises(TypeError): + pcs_new.write({"configuration_space": cs_with_forbidden}) + + +def test_read_new_configuration_space_complex_conditionals(): + classi = OrdinalHyperparameter( + "classi", + ["random_forest", "extra_trees", "k_nearest_neighbors", "something"], + ) + knn_weights = CategoricalHyperparameter("knn_weights", ["uniform", "distance"]) + weather = OrdinalHyperparameter("weather", ["sunny", "rainy", "cloudy", "snowing"]) + temperature = CategoricalHyperparameter("temperature", ["high", "low"]) + rain = CategoricalHyperparameter("rain", ["yes", "no"]) + gloves = OrdinalHyperparameter("gloves", ["none", "yarn", "leather", "gortex"]) + heur1 = CategoricalHyperparameter("heur1", ["off", "on"]) + heur2 = CategoricalHyperparameter("heur2", ["off", "on"]) + heur_order = CategoricalHyperparameter("heur_order", ["heur1then2", "heur2then1"]) + gloves_condition = OrConjunction( + EqualsCondition(gloves, rain, "yes"), + EqualsCondition(gloves, temperature, "low"), + ) + heur_condition = AndConjunction( + EqualsCondition(heur_order, heur1, "on"), + EqualsCondition(heur_order, heur2, "on"), + ) + and_conjunction = AndConjunction( + NotEqualsCondition(knn_weights, classi, "extra_trees"), + EqualsCondition(knn_weights, classi, "random_forest"), + ) + Cl_condition = OrConjunction( + EqualsCondition(knn_weights, classi, "k_nearest_neighbors"), + and_conjunction, + EqualsCondition(knn_weights, classi, "something"), + ) + + and1 = AndConjunction( + EqualsCondition(temperature, weather, "rainy"), + EqualsCondition(temperature, weather, "cloudy"), + ) + and2 = AndConjunction( + EqualsCondition(temperature, weather, "sunny"), + NotEqualsCondition(temperature, weather, "snowing"), + ) + another_condition = OrConjunction(and1, and2) + + complex_conditional_space = ConfigurationSpace() + complex_conditional_space.add_hyperparameter(classi) + complex_conditional_space.add_hyperparameter(knn_weights) + complex_conditional_space.add_hyperparameter(weather) + complex_conditional_space.add_hyperparameter(temperature) + complex_conditional_space.add_hyperparameter(rain) + complex_conditional_space.add_hyperparameter(gloves) + complex_conditional_space.add_hyperparameter(heur1) + complex_conditional_space.add_hyperparameter(heur2) + complex_conditional_space.add_hyperparameter(heur_order) + + complex_conditional_space.add_condition(gloves_condition) + complex_conditional_space.add_condition(heur_condition) + complex_conditional_space.add_condition(Cl_condition) + complex_conditional_space.add_condition(another_condition) + + complex_cs = [] + complex_cs.append( + "classi ordinal {random_forest,extra_trees,k_nearest_neighbors, something} " + "[random_forest]", + ) + complex_cs.append("knn_weights categorical {uniform, distance} [uniform]") + complex_cs.append("weather ordinal {sunny, rainy, cloudy, snowing} [sunny]") + complex_cs.append("temperature categorical {high, low} [high]") + complex_cs.append("rain categorical { yes, no } [yes]") + complex_cs.append("gloves ordinal { none, yarn, leather, gortex } [none]") + complex_cs.append("heur1 categorical { off, on } [off]") + complex_cs.append("heur2 categorical { off, on } [off]") + complex_cs.append("heur_order categorical { heur1then2, heur2then1 } [heur1then2]") + complex_cs.append("gloves | rain == yes || temperature == low") + complex_cs.append("heur_order | heur1 == on && heur2 == on") + complex_cs.append( + "knn_weights | classi == k_nearest_neighbors || " + "classi != extra_trees && classi == random_forest || classi == something", + ) + complex_cs.append( + "temperature | weather == rainy && weather == cloudy || " + "weather == sunny && weather != snowing", + ) + cs_new = pcs_new.read(complex_cs) + assert cs_new == complex_conditional_space + + +def test_convert_restrictions(): + # This is a smoke test to make sure that the int/float values in the + # greater or smaller statements are converted to the right type when + # reading them + s = """x1 real [0,1] [0] + x2 real [0,1] [0] + x3 real [0,1] [0] + x4 integer [0,2] [0] + x5 real [0,1] [0] + x6 ordinal {cold, luke-warm, hot} [cold] + x1 | x2 > 0.5 + x3 | x4 > 1 && x4 == 2 && x4 in {2} + x5 | x6 > luke-warm""" + + pcs_new.read(s.split("\n")) + + +def test_write_restrictions(): + s = ( + "c integer [0, 2] [0]\n" + + "d ordinal {cold, luke-warm, hot} [cold]\n" + + "e real [0.0, 1.0] [0.0]\n" + + "b real [0.0, 1.0] [0.0]\n" + + "a real [0.0, 1.0] [0.0]\n" + + "\n" + + "b | d in {luke-warm, hot} || c > 1\n" + + "a | b == 0.5 && e > 0.5" + ) + + a = pcs_new.read(s.split("\n")) + out = pcs_new.write(a) + assert out == s + + +def test_read_write(): + # Some smoke tests whether reading, writing, reading alters makes the + # configspace incomparable + this_file = os.path.abspath(__file__) + this_directory = os.path.dirname(this_file) + configuration_space_path = os.path.join(this_directory, "..", "test_searchspaces") + configuration_space_path = os.path.abspath(configuration_space_path) + configuration_space_path = os.path.join(configuration_space_path, "spear-params-mixed.pcs") + with open(configuration_space_path) as fh: + cs = pcs.read(fh) + + tf = tempfile.NamedTemporaryFile() + name = tf.name + tf.close() + with open(name, "w") as fh: + pcs_string = pcs.write(cs) + fh.write(pcs_string) + with open(name) as fh: + pcs_new = pcs.read(fh) + + assert pcs_new == cs, (pcs_new, cs) + + +def test_write_categorical_with_weights(): + cat = CategoricalHyperparameter("a", ["a", "b"], weights=[0.3, 0.7]) + cs = ConfigurationSpace() + cs.add_hyperparameter(cat) + with pytest.raises(ValueError, match="The pcs format does not support"): + pcs.write(cs) + + +def test_write_numerical_cond_and_forb(): + cs = ConfigurationSpace(seed=12345) + + hc1 = CategoricalHyperparameter(name="hc1", choices=[True, False], default_value=True) + hc2 = CategoricalHyperparameter(name="hc2", choices=[True, False], default_value=True) + + hf1 = UniformFloatHyperparameter(name="hf1", lower=1.0, upper=10.0, default_value=5.0) + hi1 = UniformIntegerHyperparameter(name="hi1", lower=1, upper=10, default_value=5) + cs.add_hyperparameters([hc1, hc2, hf1, hi1]) + c1 = InCondition(child=hc1, parent=hc2, values=[True]) + c2 = InCondition(child=hc2, parent=hi1, values=[1, 2, 3, 4]) + c3 = GreaterThanCondition(hi1, hf1, 6.0) + c4 = EqualsCondition(hi1, hf1, 8.0) + c5 = AndConjunction(c3, c4) + + cs.add_conditions([c1, c2, c5]) + + f1 = ForbiddenEqualsClause(hc1, False) + f2 = ForbiddenEqualsClause(hf1, 2.0) + f3 = ForbiddenEqualsClause(hi1, 3) + cs.add_forbidden_clauses([ForbiddenAndConjunction(f2, f3), f1]) + expected = ( + "hf1 real [1.0, 10.0] [5.0]\n" + + "hi1 integer [1, 10] [5]\n" + + "hc2 categorical {True, False} [True]\n" + + "hc1 categorical {True, False} [True]\n" + + "\n" + + "hi1 | hf1 > 6.0 && hf1 == 8.0\n" + + "hc2 | hi1 in {1, 2, 3, 4}\n" + + "hc1 | hc2 in {True}\n" + + "\n" + + "{hc1=False}\n" + + "{hf1=2.0, hi1=3}\n" + ) + out = pcs_new.write(cs) + assert out == expected diff --git a/test/test_conditions.py b/test/test_conditions.py index b43f7342..ede9ff1f 100644 --- a/test/test_conditions.py +++ b/test/test_conditions.py @@ -27,9 +27,8 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import annotations -import unittest - import numpy as np +import pytest from ConfigSpace.conditions import ( AndConjunction, @@ -49,332 +48,306 @@ ) -class TestConditions(unittest.TestCase): - # TODO: return only copies of the objects! - def test_equals_condition(self): - hp1 = CategoricalHyperparameter("parent", [0, 1]) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cond = EqualsCondition(hp2, hp1, 0) - cond_ = EqualsCondition(hp2, hp1, 0) - - # Test vector value: - assert cond.vector_value == hp1._inverse_transform(0) - assert cond.vector_value == cond_.vector_value - - # Test invalid conditions: - self.assertRaises(TypeError, EqualsCondition, hp2, "parent", 0) - self.assertRaises(TypeError, EqualsCondition, "child", hp1, 0) - self.assertRaises(ValueError, EqualsCondition, hp1, hp1, 0) - - assert cond == cond_ - - cond_reverse = EqualsCondition(hp1, hp2, 0) - assert cond != cond_reverse - - assert cond != {} - - assert str(cond) == "child | parent == 0" - - def test_equals_condition_illegal_value(self): - epsilon = UniformFloatHyperparameter("epsilon", 1e-5, 1e-1, default_value=1e-4, log=True) - loss = CategoricalHyperparameter( - "loss", - ["hinge", "log", "modified_huber", "squared_hinge", "perceptron"], - default_value="hinge", - ) - self.assertRaisesRegex( - ValueError, - "Hyperparameter 'epsilon' is " - "conditional on the illegal value 'huber' of " - "its parent hyperparameter 'loss'", - EqualsCondition, - epsilon, - loss, - "huber", - ) - - def test_not_equals_condition(self): - hp1 = CategoricalHyperparameter("parent", [0, 1]) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cond = NotEqualsCondition(hp2, hp1, 0) - cond_ = NotEqualsCondition(hp2, hp1, 0) - assert cond == cond_ - - # Test vector value: - assert cond.vector_value == hp1._inverse_transform(0) - assert cond.vector_value == cond_.vector_value - - cond_reverse = NotEqualsCondition(hp1, hp2, 0) - assert cond != cond_reverse - - assert cond != {} - - assert str(cond) == "child | parent != 0" - - def test_not_equals_condition_illegal_value(self): - epsilon = UniformFloatHyperparameter("epsilon", 1e-5, 1e-1, default_value=1e-4, log=True) - loss = CategoricalHyperparameter( - "loss", - ["hinge", "log", "modified_huber", "squared_hinge", "perceptron"], - default_value="hinge", - ) - self.assertRaisesRegex( - ValueError, - "Hyperparameter 'epsilon' is " - "conditional on the illegal value 'huber' of " - "its parent hyperparameter 'loss'", - NotEqualsCondition, - epsilon, - loss, - "huber", - ) - - def test_in_condition(self): - hp1 = CategoricalHyperparameter("parent", list(range(0, 11))) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cond = InCondition(hp2, hp1, [0, 1, 2, 3, 4, 5]) - cond_ = InCondition(hp2, hp1, [0, 1, 2, 3, 4, 5]) - assert cond == cond_ - - # Test vector value: - assert cond.vector_values == [hp1._inverse_transform(i) for i in [0, 1, 2, 3, 4, 5]] - assert cond.vector_values == cond_.vector_values - - cond_reverse = InCondition(hp1, hp2, [0, 1, 2, 3, 4, 5]) - assert cond != cond_reverse - - assert cond != {} - - assert str(cond) == "child | parent in {0, 1, 2, 3, 4, 5}" - - def test_greater_and_less_condition(self): - child = Constant("child", "child") - hp1 = UniformFloatHyperparameter("float", 0, 5) - hp2 = UniformIntegerHyperparameter("int", 0, 5) - hp3 = OrdinalHyperparameter("ord", list(range(6))) - - for hp in [hp1, hp2, hp3]: - hyperparameter_idx = {child.name: 0, hp.name: 1} - - gt = GreaterThanCondition(child, hp, 1) - gt.set_vector_idx(hyperparameter_idx) - assert not gt.evaluate({hp.name: 0}) - assert gt.evaluate({hp.name: 2}) - assert not gt.evaluate({hp.name: None}) - - # Evaluate vector - test_value = hp._inverse_transform(2) - assert not gt.evaluate_vector(np.array([np.NaN, 0])) - assert gt.evaluate_vector(np.array([np.NaN, test_value])) - assert not gt.evaluate_vector(np.array([np.NaN, np.NaN])) - - lt = LessThanCondition(child, hp, 1) - lt.set_vector_idx(hyperparameter_idx) - assert lt.evaluate({hp.name: 0}) - assert not lt.evaluate({hp.name: 2}) - assert not lt.evaluate({hp.name: None}) - - # Evaluate vector - test_value = hp._inverse_transform(2) - assert lt.evaluate_vector(np.array([np.NaN, 0, 0, 0])) - assert not lt.evaluate_vector(np.array([np.NaN, test_value])) - assert not lt.evaluate_vector(np.array([np.NaN, np.NaN])) - - hp4 = CategoricalHyperparameter("cat", list(range(6))) - self.assertRaisesRegex( - ValueError, - "Parent hyperparameter in a > or < condition must be a subclass of " - "NumericalHyperparameter or OrdinalHyperparameter, but is " - "", - GreaterThanCondition, - child, - hp4, - 1, - ) - self.assertRaisesRegex( - ValueError, - "Parent hyperparameter in a > or < condition must be a subclass of " - "NumericalHyperparameter or OrdinalHyperparameter, but is " - "", - LessThanCondition, - child, - hp4, - 1, - ) - - hp5 = OrdinalHyperparameter("ord", ["cold", "luke warm", "warm", "hot"]) - - hyperparameter_idx = {child.name: 0, hp5.name: 1} - gt = GreaterThanCondition(child, hp5, "warm") +# TODO: return only copies of the objects! +def test_equals_condition(): + hp1 = CategoricalHyperparameter("parent", [0, 1]) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cond = EqualsCondition(hp2, hp1, 0) + cond_ = EqualsCondition(hp2, hp1, 0) + + # Test vector value: + assert cond.vector_value == hp1._inverse_transform(0) + assert cond.vector_value == cond_.vector_value + + # Test invalid conditions: + with pytest.raises(TypeError): + EqualsCondition(hp2, "parent", 0) + with pytest.raises(TypeError): + EqualsCondition("child", hp1, 0) + with pytest.raises(ValueError): + EqualsCondition(hp1, hp1, 0) + + assert cond == cond_ + + cond_reverse = EqualsCondition(hp1, hp2, 0) + assert cond != cond_reverse + + assert cond != {} + + assert str(cond) == "child | parent == 0" + + +def test_equals_condition_illegal_value(): + epsilon = UniformFloatHyperparameter("epsilon", 1e-5, 1e-1, default_value=1e-4, log=True) + loss = CategoricalHyperparameter( + "loss", + ["hinge", "log", "modified_huber", "squared_hinge", "perceptron"], + default_value="hinge", + ) + with pytest.raises(ValueError): + EqualsCondition(epsilon, loss, "huber") + + +def test_not_equals_condition(): + hp1 = CategoricalHyperparameter("parent", [0, 1]) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cond = NotEqualsCondition(hp2, hp1, 0) + cond_ = NotEqualsCondition(hp2, hp1, 0) + assert cond == cond_ + + # Test vector value: + assert cond.vector_value == hp1._inverse_transform(0) + assert cond.vector_value == cond_.vector_value + + cond_reverse = NotEqualsCondition(hp1, hp2, 0) + assert cond != cond_reverse + + assert cond != {} + + assert str(cond) == "child | parent != 0" + + +def test_not_equals_condition_illegal_value(): + epsilon = UniformFloatHyperparameter("epsilon", 1e-5, 1e-1, default_value=1e-4, log=True) + loss = CategoricalHyperparameter( + "loss", + ["hinge", "log", "modified_huber", "squared_hinge", "perceptron"], + default_value="hinge", + ) + with pytest.raises(ValueError): + NotEqualsCondition(epsilon, loss, "huber") + + +def test_in_condition(): + hp1 = CategoricalHyperparameter("parent", list(range(11))) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cond = InCondition(hp2, hp1, [0, 1, 2, 3, 4, 5]) + cond_ = InCondition(hp2, hp1, [0, 1, 2, 3, 4, 5]) + assert cond == cond_ + + # Test vector value: + assert cond.vector_values == [hp1._inverse_transform(i) for i in [0, 1, 2, 3, 4, 5]] + assert cond.vector_values == cond_.vector_values + + cond_reverse = InCondition(hp1, hp2, [0, 1, 2, 3, 4, 5]) + assert cond != cond_reverse + + assert cond != {} + + assert str(cond) == "child | parent in {0, 1, 2, 3, 4, 5}" + + +def test_greater_and_less_condition(): + child = Constant("child", "child") + hp1 = UniformFloatHyperparameter("float", 0, 5) + hp2 = UniformIntegerHyperparameter("int", 0, 5) + hp3 = OrdinalHyperparameter("ord", list(range(6))) + + for hp in [hp1, hp2, hp3]: + hyperparameter_idx = {child.name: 0, hp.name: 1} + + gt = GreaterThanCondition(child, hp, 1) gt.set_vector_idx(hyperparameter_idx) - assert gt.evaluate({hp5.name: "hot"}) - assert not gt.evaluate({hp5.name: "cold"}) + assert not gt.evaluate({hp.name: 0}) + assert gt.evaluate({hp.name: 2}) + assert not gt.evaluate({hp.name: None}) - assert gt.evaluate_vector(np.array([np.NaN, 3])) + # Evaluate vector + test_value = hp._inverse_transform(2) assert not gt.evaluate_vector(np.array([np.NaN, 0])) + assert gt.evaluate_vector(np.array([np.NaN, test_value])) + assert not gt.evaluate_vector(np.array([np.NaN, np.NaN])) - lt = LessThanCondition(child, hp5, "warm") + lt = LessThanCondition(child, hp, 1) lt.set_vector_idx(hyperparameter_idx) - assert lt.evaluate({hp5.name: "luke warm"}) - assert not lt.evaluate({hp5.name: "warm"}) - - assert lt.evaluate_vector(np.array([np.NaN, 1])) - assert not lt.evaluate_vector(np.array([np.NaN, 2])) - - def test_in_condition_illegal_value(self): - epsilon = UniformFloatHyperparameter("epsilon", 1e-5, 1e-1, default_value=1e-4, log=True) - loss = CategoricalHyperparameter( - "loss", - ["hinge", "log", "modified_huber", "squared_hinge", "perceptron"], - default_value="hinge", - ) - self.assertRaisesRegex( - ValueError, - "Hyperparameter 'epsilon' is " - "conditional on the illegal value 'huber' of " - "its parent hyperparameter 'loss'", - InCondition, - epsilon, - loss, - ["huber", "log"], - ) - - def test_and_conjunction(self): - self.assertRaises(TypeError, AndConjunction, "String1", "String2") - - hp1 = CategoricalHyperparameter("input1", [0, 1]) - hp2 = CategoricalHyperparameter("input2", [0, 1]) - hp3 = CategoricalHyperparameter("input3", [0, 1]) - hp4 = Constant("And", "True") - cond1 = EqualsCondition(hp4, hp1, 1) - - # Only one condition in an AndConjunction! - self.assertRaises(ValueError, AndConjunction, cond1) - - cond2 = EqualsCondition(hp4, hp2, 1) - cond3 = EqualsCondition(hp4, hp3, 1) - - andconj1 = AndConjunction(cond1, cond2) - andconj1_ = AndConjunction(cond1, cond2) - assert andconj1 == andconj1_ - - # Test setting vector idx - hyperparameter_idx = {hp1.name: 0, hp2.name: 1, hp3.name: 2, hp4.name: 3} - andconj1.set_vector_idx(hyperparameter_idx) - assert andconj1.get_parents_vector() == [0, 1] - assert andconj1.get_children_vector() == [3, 3] - - andconj2 = AndConjunction(cond2, cond3) - assert andconj1 != andconj2 - - andconj3 = AndConjunction(cond1, cond2, cond3) - assert str(andconj3) == "(And | input1 == 1 && And | input2 == 1 && And | input3 == 1)" - - # Test __eq__ - assert andconj1 != andconj3 - assert andconj1 != "String" - - def test_or_conjunction(self): - self.assertRaises(TypeError, AndConjunction, "String1", "String2") - - hp1 = CategoricalHyperparameter("input1", [0, 1]) - hp2 = CategoricalHyperparameter("input2", [0, 1]) - hp3 = CategoricalHyperparameter("input3", [0, 1]) - hp4 = Constant("Or", "True") - cond1 = EqualsCondition(hp4, hp1, 1) - - self.assertRaises(ValueError, OrConjunction, cond1) - - cond2 = EqualsCondition(hp4, hp2, 1) - cond3 = EqualsCondition(hp4, hp3, 1) - - andconj1 = OrConjunction(cond1, cond2) - andconj1_ = OrConjunction(cond1, cond2) - assert andconj1 == andconj1_ - - # Test setting vector idx - hyperparameter_idx = {hp1.name: 0, hp2.name: 1, hp3.name: 2, hp4.name: 3} - andconj1.set_vector_idx(hyperparameter_idx) - assert andconj1.get_parents_vector() == [0, 1] - assert andconj1.get_children_vector() == [3, 3] - - andconj2 = OrConjunction(cond2, cond3) - assert andconj1 != andconj2 - - andconj3 = OrConjunction(cond1, cond2, cond3) - assert str(andconj3) == "(Or | input1 == 1 || Or | input2 == 1 || Or | input3 == 1)" - - def test_nested_conjunctions(self): - hp1 = CategoricalHyperparameter("input1", [0, 1]) - hp2 = CategoricalHyperparameter("input2", [0, 1]) - hp3 = CategoricalHyperparameter("input3", [0, 1]) - hp4 = CategoricalHyperparameter("input4", [0, 1]) - hp5 = CategoricalHyperparameter("input5", [0, 1]) - hp6 = Constant("AND", "True") - - cond1 = EqualsCondition(hp6, hp1, 1) - cond2 = EqualsCondition(hp6, hp2, 1) - cond3 = EqualsCondition(hp6, hp3, 1) - cond4 = EqualsCondition(hp6, hp4, 1) - cond5 = EqualsCondition(hp6, hp5, 1) - - conj1 = AndConjunction(cond1, cond2) - conj2 = OrConjunction(conj1, cond3) - conj3 = AndConjunction(conj2, cond4, cond5) - - # TODO: this does not look nice, And should depend on a large - # conjunction, there should not be many ANDs inside this string! - assert ( - str(conj3) - == "(((AND | input1 == 1 && AND | input2 == 1) || AND | input3 == 1) && AND | input4 == 1 && AND | input5 == 1)" - ) - - def test_all_components_have_the_same_child(self): - hp1 = CategoricalHyperparameter("input1", [0, 1]) - hp2 = CategoricalHyperparameter("input2", [0, 1]) - hp3 = CategoricalHyperparameter("input3", [0, 1]) - hp4 = CategoricalHyperparameter("input4", [0, 1]) - hp5 = CategoricalHyperparameter("input5", [0, 1]) - hp6 = Constant("AND", "True") - - cond1 = EqualsCondition(hp1, hp2, 1) - cond2 = EqualsCondition(hp1, hp3, 1) - cond3 = EqualsCondition(hp1, hp4, 1) - cond4 = EqualsCondition(hp6, hp4, 1) - cond5 = EqualsCondition(hp6, hp5, 1) - - AndConjunction(cond1, cond2, cond3) - AndConjunction(cond4, cond5) - self.assertRaisesRegex( - ValueError, - "All Conjunctions and Conditions must have " "the same child.", - AndConjunction, - cond1, - cond4, - ) - - def test_condition_from_cryptominisat(self): - parent = CategoricalHyperparameter("blkrest", ["0", "1"], default_value="1") - child = UniformIntegerHyperparameter("blkrestlen", 2000, 10000, log=True) - condition = EqualsCondition(child, parent, "1") - assert not condition.evaluate({"blkrest": "0"}) - assert condition.evaluate({"blkrest": "1"}) - - def test_get_parents(self): - # Necessary because we couldn't call cs.get_parents for - # clasp-sat-params-nat.pcs - counter = UniformIntegerHyperparameter("bump", 10, 4096, log=True) - _1_S_countercond = CategoricalHyperparameter("cony", ["yes", "no"]) - _1_0_restarts = CategoricalHyperparameter( - "restarts", - ["F", "L", "D", "x", "+", "no"], - default_value="x", - ) - - condition = EqualsCondition(counter, _1_S_countercond, "yes") - # All conditions inherit get_parents from abstractcondition - assert [_1_S_countercond] == condition.get_parents() - condition2 = InCondition(counter, _1_0_restarts, ["F", "D", "L", "x", "+"]) - # All conjunctions inherit get_parents from abstractconjunction - conjunction = AndConjunction(condition, condition2) - assert [_1_S_countercond, _1_0_restarts] == conjunction.get_parents() + assert lt.evaluate({hp.name: 0}) + assert not lt.evaluate({hp.name: 2}) + assert not lt.evaluate({hp.name: None}) + + # Evaluate vector + test_value = hp._inverse_transform(2) + assert lt.evaluate_vector(np.array([np.NaN, 0, 0, 0])) + assert not lt.evaluate_vector(np.array([np.NaN, test_value])) + assert not lt.evaluate_vector(np.array([np.NaN, np.NaN])) + + hp4 = CategoricalHyperparameter("cat", list(range(6))) + with pytest.raises(ValueError): + GreaterThanCondition(child, hp4, 1) + + with pytest.raises(ValueError): + LessThanCondition(child, hp4, 1) + + hp5 = OrdinalHyperparameter("ord", ["cold", "luke warm", "warm", "hot"]) + + hyperparameter_idx = {child.name: 0, hp5.name: 1} + gt = GreaterThanCondition(child, hp5, "warm") + gt.set_vector_idx(hyperparameter_idx) + assert gt.evaluate({hp5.name: "hot"}) + assert not gt.evaluate({hp5.name: "cold"}) + + assert gt.evaluate_vector(np.array([np.NaN, 3])) + assert not gt.evaluate_vector(np.array([np.NaN, 0])) + + lt = LessThanCondition(child, hp5, "warm") + lt.set_vector_idx(hyperparameter_idx) + assert lt.evaluate({hp5.name: "luke warm"}) + assert not lt.evaluate({hp5.name: "warm"}) + + assert lt.evaluate_vector(np.array([np.NaN, 1])) + assert not lt.evaluate_vector(np.array([np.NaN, 2])) + + +def test_in_condition_illegal_value(): + epsilon = UniformFloatHyperparameter("epsilon", 1e-5, 1e-1, default_value=1e-4, log=True) + loss = CategoricalHyperparameter( + "loss", + ["hinge", "log", "modified_huber", "squared_hinge", "perceptron"], + default_value="hinge", + ) + with pytest.raises(ValueError): + InCondition(epsilon, loss, ["huber", "log"]) + + +def test_and_conjunction(): + with pytest.raises(TypeError): + AndConjunction("String1", "String2") + + hp1 = CategoricalHyperparameter("input1", [0, 1]) + hp2 = CategoricalHyperparameter("input2", [0, 1]) + hp3 = CategoricalHyperparameter("input3", [0, 1]) + hp4 = Constant("And", "True") + cond1 = EqualsCondition(hp4, hp1, 1) + + # Only one condition in an AndConjunction! + with pytest.raises(ValueError): + AndConjunction(cond1) + + cond2 = EqualsCondition(hp4, hp2, 1) + cond3 = EqualsCondition(hp4, hp3, 1) + + andconj1 = AndConjunction(cond1, cond2) + andconj1_ = AndConjunction(cond1, cond2) + assert andconj1 == andconj1_ + + # Test setting vector idx + hyperparameter_idx = {hp1.name: 0, hp2.name: 1, hp3.name: 2, hp4.name: 3} + andconj1.set_vector_idx(hyperparameter_idx) + assert andconj1.get_parents_vector() == [0, 1] + assert andconj1.get_children_vector() == [3, 3] + + andconj2 = AndConjunction(cond2, cond3) + assert andconj1 != andconj2 + + andconj3 = AndConjunction(cond1, cond2, cond3) + assert str(andconj3) == "(And | input1 == 1 && And | input2 == 1 && And | input3 == 1)" + + # Test __eq__ + assert andconj1 != andconj3 + assert andconj1 != "String" + + +def test_or_conjunction(): + with pytest.raises(TypeError): + OrConjunction("String1", "String2") + + hp1 = CategoricalHyperparameter("input1", [0, 1]) + hp2 = CategoricalHyperparameter("input2", [0, 1]) + hp3 = CategoricalHyperparameter("input3", [0, 1]) + hp4 = Constant("Or", "True") + cond1 = EqualsCondition(hp4, hp1, 1) + + with pytest.raises(ValueError): + OrConjunction(cond1) + + cond2 = EqualsCondition(hp4, hp2, 1) + cond3 = EqualsCondition(hp4, hp3, 1) + + andconj1 = OrConjunction(cond1, cond2) + andconj1_ = OrConjunction(cond1, cond2) + assert andconj1 == andconj1_ + + # Test setting vector idx + hyperparameter_idx = {hp1.name: 0, hp2.name: 1, hp3.name: 2, hp4.name: 3} + andconj1.set_vector_idx(hyperparameter_idx) + assert andconj1.get_parents_vector() == [0, 1] + assert andconj1.get_children_vector() == [3, 3] + + andconj2 = OrConjunction(cond2, cond3) + assert andconj1 != andconj2 + + andconj3 = OrConjunction(cond1, cond2, cond3) + assert str(andconj3) == "(Or | input1 == 1 || Or | input2 == 1 || Or | input3 == 1)" + + +def test_nested_conjunctions(): + hp1 = CategoricalHyperparameter("input1", [0, 1]) + hp2 = CategoricalHyperparameter("input2", [0, 1]) + hp3 = CategoricalHyperparameter("input3", [0, 1]) + hp4 = CategoricalHyperparameter("input4", [0, 1]) + hp5 = CategoricalHyperparameter("input5", [0, 1]) + hp6 = Constant("AND", "True") + + cond1 = EqualsCondition(hp6, hp1, 1) + cond2 = EqualsCondition(hp6, hp2, 1) + cond3 = EqualsCondition(hp6, hp3, 1) + cond4 = EqualsCondition(hp6, hp4, 1) + cond5 = EqualsCondition(hp6, hp5, 1) + + conj1 = AndConjunction(cond1, cond2) + conj2 = OrConjunction(conj1, cond3) + conj3 = AndConjunction(conj2, cond4, cond5) + + # TODO: this does not look nice, And should depend on a large + # conjunction, there should not be many ANDs inside this string! + assert ( + str(conj3) + == "(((AND | input1 == 1 && AND | input2 == 1) || AND | input3 == 1) && AND | input4 == 1 && AND | input5 == 1)" + ) + + +def test_all_components_have_the_same_child(): + hp1 = CategoricalHyperparameter("input1", [0, 1]) + hp2 = CategoricalHyperparameter("input2", [0, 1]) + hp3 = CategoricalHyperparameter("input3", [0, 1]) + hp4 = CategoricalHyperparameter("input4", [0, 1]) + hp5 = CategoricalHyperparameter("input5", [0, 1]) + hp6 = Constant("AND", "True") + + cond1 = EqualsCondition(hp1, hp2, 1) + cond2 = EqualsCondition(hp1, hp3, 1) + cond3 = EqualsCondition(hp1, hp4, 1) + cond4 = EqualsCondition(hp6, hp4, 1) + cond5 = EqualsCondition(hp6, hp5, 1) + + AndConjunction(cond1, cond2, cond3) + AndConjunction(cond4, cond5) + with pytest.raises(ValueError): + AndConjunction(cond1, cond4) + + +def test_condition_from_cryptominisat(): + parent = CategoricalHyperparameter("blkrest", ["0", "1"], default_value="1") + child = UniformIntegerHyperparameter("blkrestlen", 2000, 10000, log=True) + condition = EqualsCondition(child, parent, "1") + assert not condition.evaluate({"blkrest": "0"}) + assert condition.evaluate({"blkrest": "1"}) + + +def test_get_parents(): + # Necessary because we couldn't call cs.get_parents for + # clasp-sat-params-nat.pcs + counter = UniformIntegerHyperparameter("bump", 10, 4096, log=True) + _1_S_countercond = CategoricalHyperparameter("cony", ["yes", "no"]) + _1_0_restarts = CategoricalHyperparameter( + "restarts", + ["F", "L", "D", "x", "+", "no"], + default_value="x", + ) + + condition = EqualsCondition(counter, _1_S_countercond, "yes") + # All conditions inherit get_parents from abstractcondition + assert [_1_S_countercond] == condition.get_parents() + condition2 = InCondition(counter, _1_0_restarts, ["F", "D", "L", "x", "+"]) + # All conjunctions inherit get_parents from abstractconjunction + conjunction = AndConjunction(condition, condition2) + assert [_1_S_countercond, _1_0_restarts] == conjunction.get_parents() diff --git a/test/test_configuration_space.py b/test/test_configuration_space.py index d850360d..0152252e 100644 --- a/test/test_configuration_space.py +++ b/test/test_configuration_space.py @@ -33,6 +33,7 @@ from itertools import product import numpy as np +import pytest from ConfigSpace import ( AndConjunction, @@ -74,403 +75,278 @@ def byteify(input): return input -class TestConfigurationSpace(unittest.TestCase): - # TODO generalize a few simple configuration spaces which are used over - # and over again throughout this test suite - # TODO make sure that every function here tests one aspect of the - # configuration space object! - def test_add_hyperparameter(self): - cs = ConfigurationSpace() - hp = UniformIntegerHyperparameter("name", 0, 10) - cs.add_hyperparameter(hp) - - def test_add_non_hyperparameter(self): - cs = ConfigurationSpace() - non_hp = unittest.TestSuite() - - with self.assertRaises(TypeError): - cs.add_hyperparameter(non_hp) - - def test_add_hyperparameters_with_equal_names(self): - cs = ConfigurationSpace() - hp = UniformIntegerHyperparameter("name", 0, 10) - cs.add_hyperparameter(hp) - with self.assertRaises(HyperparameterAlreadyExistsError): - cs.add_hyperparameter(hp) - - def test_illegal_default_configuration(self): - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("loss", ["l1", "l2"], default_value="l1") - hp2 = CategoricalHyperparameter("penalty", ["l1", "l2"], default_value="l1") - cs.add_hyperparameter(hp1) - cs.add_hyperparameter(hp2) - forb1 = ForbiddenEqualsClause(hp1, "l1") - forb2 = ForbiddenEqualsClause(hp2, "l1") - forb3 = ForbiddenAndConjunction(forb1, forb2) - - with self.assertRaises(ForbiddenValueError): - cs.add_forbidden_clause(forb3) - - def test_meta_data_stored(self): - meta_data = { - "additional": "meta-data", - "useful": "for integrations", - "input_id": 42, - } - cs = ConfigurationSpace(meta=dict(meta_data)) - assert cs.meta == meta_data - - def test_add_non_condition(self): - cs = ConfigurationSpace() - non_cond = unittest.TestSuite() - with self.assertRaises(TypeError): - cs.add_condition(non_cond) - - def test_hyperparameters_with_valid_condition(self): - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("parent", [0, 1]) - cs.add_hyperparameter(hp1) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cs.add_hyperparameter(hp2) - cond = EqualsCondition(hp2, hp1, 0) - cs.add_condition(cond) - assert len(cs._hyperparameters) == 2 +def test_add_hyperparameter(): + cs = ConfigurationSpace() + hp = UniformIntegerHyperparameter("name", 0, 10) + cs.add_hyperparameter(hp) - def test_condition_without_added_hyperparameters(self): - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("parent", [0, 1]) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cond = EqualsCondition(hp2, hp1, 0) - with self.assertRaises(ChildNotFoundError): - cs.add_condition(cond) +def test_add_non_hyperparameter(): + cs = ConfigurationSpace() + non_hp = unittest.TestSuite() - cs.add_hyperparameter(hp1) + with pytest.raises(TypeError): + cs.add_hyperparameter(non_hp) - with self.assertRaises(ChildNotFoundError): - cs.add_condition(cond) - # Test also the parent hyperparameter - cs2 = ConfigurationSpace() - cs2.add_hyperparameter(hp2) +def test_add_hyperparameters_with_equal_names(): + cs = ConfigurationSpace() + hp = UniformIntegerHyperparameter("name", 0, 10) + cs.add_hyperparameter(hp) + with pytest.raises(HyperparameterAlreadyExistsError): + cs.add_hyperparameter(hp) - with self.assertRaises(ParentNotFoundError): - cs2.add_condition(cond) - def test_condition_with_cycles(self): - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("parent", [0, 1]) - cs.add_hyperparameter(hp1) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cs.add_hyperparameter(hp2) - cond1 = EqualsCondition(hp2, hp1, 0) - cs.add_condition(cond1) - cond2 = EqualsCondition(hp1, hp2, 0) +def test_illegal_default_configuration(): + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("loss", ["l1", "l2"], default_value="l1") + hp2 = CategoricalHyperparameter("penalty", ["l1", "l2"], default_value="l1") + cs.add_hyperparameter(hp1) + cs.add_hyperparameter(hp2) + forb1 = ForbiddenEqualsClause(hp1, "l1") + forb2 = ForbiddenEqualsClause(hp2, "l1") + forb3 = ForbiddenAndConjunction(forb1, forb2) - with self.assertRaises(CyclicDependancyError): - cs.add_condition(cond2) + with pytest.raises(ForbiddenValueError): + cs.add_forbidden_clause(forb3) - def test_add_conjunction(self): - hp1 = CategoricalHyperparameter("input1", [0, 1]) - hp2 = CategoricalHyperparameter("input2", [0, 1]) - hp3 = CategoricalHyperparameter("input3", [0, 1]) - hp4 = Constant("And", "True") - cond1 = EqualsCondition(hp4, hp1, 1) - cond2 = EqualsCondition(hp4, hp2, 1) - cond3 = EqualsCondition(hp4, hp3, 1) +def test_meta_data_stored(): + meta_data = { + "additional": "meta-data", + "useful": "for integrations", + "input_id": 42, + } + cs = ConfigurationSpace(meta=dict(meta_data)) + assert cs.meta == meta_data - andconj1 = AndConjunction(cond1, cond2, cond3) - cs = ConfigurationSpace() - cs.add_hyperparameter(hp1) - cs.add_hyperparameter(hp2) - cs.add_hyperparameter(hp3) - cs.add_hyperparameter(hp4) +def test_add_non_condition(): + cs = ConfigurationSpace() + non_cond = unittest.TestSuite() + with pytest.raises(TypeError): + cs.add_condition(non_cond) - cs.add_condition(andconj1) - assert hp4 not in cs.get_all_unconditional_hyperparameters() - def test_add_second_condition_wo_conjunction(self): - hp1 = CategoricalHyperparameter("input1", [0, 1]) - hp2 = CategoricalHyperparameter("input2", [0, 1]) - hp3 = Constant("And", "True") +def test_hyperparameters_with_valid_condition(): + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("parent", [0, 1]) + cs.add_hyperparameter(hp1) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cs.add_hyperparameter(hp2) + cond = EqualsCondition(hp2, hp1, 0) + cs.add_condition(cond) + assert len(cs._hyperparameters) == 2 - cond1 = EqualsCondition(hp3, hp1, 1) - cond2 = EqualsCondition(hp3, hp2, 1) - cs = ConfigurationSpace() - cs.add_hyperparameter(hp1) - cs.add_hyperparameter(hp2) - cs.add_hyperparameter(hp3) +def test_condition_without_added_hyperparameters(): + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("parent", [0, 1]) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cond = EqualsCondition(hp2, hp1, 0) - cs.add_condition(cond1) + with pytest.raises(ChildNotFoundError): + cs.add_condition(cond) - with self.assertRaises(AmbiguousConditionError): - cs.add_condition(cond2) + cs.add_hyperparameter(hp1) - def test_add_forbidden_clause(self): - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("input1", [0, 1]) - cs.add_hyperparameter(hp1) - forb = ForbiddenEqualsClause(hp1, 1) - # TODO add checking whether a forbidden clause makes sense at all - cs.add_forbidden_clause(forb) - # TODO add something to properly retrieve the forbidden clauses - assert ( - str(cs) - == "Configuration space object:\n Hyperparameters:\n input1, Type: Categorical, Choices: {0, 1}, Default: 0\n Forbidden Clauses:\n Forbidden: input1 == 1\n" - ) + with pytest.raises(ChildNotFoundError): + cs.add_condition(cond) - def test_add_forbidden_relation(self): - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("input1", [0, 1]) - hp2 = CategoricalHyperparameter("input2", [1, 0]) - cs.add_hyperparameters([hp1, hp2]) - forb = ForbiddenEqualsRelation(hp1, hp2) - # TODO add checking whether a forbidden clause makes sense at all + # Test also the parent hyperparameter + cs2 = ConfigurationSpace() + cs2.add_hyperparameter(hp2) + + with pytest.raises(ParentNotFoundError): + cs2.add_condition(cond) + + +def test_condition_with_cycles(): + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("parent", [0, 1]) + cs.add_hyperparameter(hp1) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cs.add_hyperparameter(hp2) + cond1 = EqualsCondition(hp2, hp1, 0) + cs.add_condition(cond1) + cond2 = EqualsCondition(hp1, hp2, 0) + + with pytest.raises(CyclicDependancyError): + cs.add_condition(cond2) + + +def test_add_conjunction(): + hp1 = CategoricalHyperparameter("input1", [0, 1]) + hp2 = CategoricalHyperparameter("input2", [0, 1]) + hp3 = CategoricalHyperparameter("input3", [0, 1]) + hp4 = Constant("And", "True") + + cond1 = EqualsCondition(hp4, hp1, 1) + cond2 = EqualsCondition(hp4, hp2, 1) + cond3 = EqualsCondition(hp4, hp3, 1) + + andconj1 = AndConjunction(cond1, cond2, cond3) + + cs = ConfigurationSpace() + cs.add_hyperparameter(hp1) + cs.add_hyperparameter(hp2) + cs.add_hyperparameter(hp3) + cs.add_hyperparameter(hp4) + + cs.add_condition(andconj1) + assert hp4 not in cs.get_all_unconditional_hyperparameters() + + +def test_add_second_condition_wo_conjunction(): + hp1 = CategoricalHyperparameter("input1", [0, 1]) + hp2 = CategoricalHyperparameter("input2", [0, 1]) + hp3 = Constant("And", "True") + + cond1 = EqualsCondition(hp3, hp1, 1) + cond2 = EqualsCondition(hp3, hp2, 1) + + cs = ConfigurationSpace() + cs.add_hyperparameter(hp1) + cs.add_hyperparameter(hp2) + cs.add_hyperparameter(hp3) + + cs.add_condition(cond1) + + with pytest.raises(AmbiguousConditionError): + cs.add_condition(cond2) + + +def test_add_forbidden_clause(): + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("input1", [0, 1]) + cs.add_hyperparameter(hp1) + forb = ForbiddenEqualsClause(hp1, 1) + # TODO add checking whether a forbidden clause makes sense at all + cs.add_forbidden_clause(forb) + # TODO add something to properly retrieve the forbidden clauses + assert ( + str(cs) + == "Configuration space object:\n Hyperparameters:\n input1, Type: Categorical, Choices: {0, 1}, Default: 0\n Forbidden Clauses:\n Forbidden: input1 == 1\n" + ) + + +def test_add_forbidden_relation(): + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("input1", [0, 1]) + hp2 = CategoricalHyperparameter("input2", [1, 0]) + cs.add_hyperparameters([hp1, hp2]) + forb = ForbiddenEqualsRelation(hp1, hp2) + # TODO add checking whether a forbidden clause makes sense at all + cs.add_forbidden_clause(forb) + # TODO add something to properly retrieve the forbidden clauses + assert ( + str(cs) + == "Configuration space object:\n Hyperparameters:\n input1, Type: Categorical, Choices: {0, 1}, Default: 0\n input2, Type: Categorical, Choices: {1, 0}, Default: 1\n Forbidden Clauses:\n Forbidden: input1 == input2\n" + ) + + +def test_add_forbidden_relation_categorical(): + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("input1", ["a", "b"], default_value="b") + hp2 = CategoricalHyperparameter("input2", ["b", "c"], default_value="b") + cs.add_hyperparameters([hp1, hp2]) + forb = ForbiddenEqualsRelation(hp1, hp2) + with pytest.raises(ForbiddenValueError): cs.add_forbidden_clause(forb) - # TODO add something to properly retrieve the forbidden clauses - assert ( - str(cs) - == "Configuration space object:\n Hyperparameters:\n input1, Type: Categorical, Choices: {0, 1}, Default: 0\n input2, Type: Categorical, Choices: {1, 0}, Default: 1\n Forbidden Clauses:\n Forbidden: input1 == input2\n" - ) - - def test_add_forbidden_relation_categorical(self): - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("input1", ["a", "b"], default_value="b") - hp2 = CategoricalHyperparameter("input2", ["b", "c"], default_value="b") - cs.add_hyperparameters([hp1, hp2]) - forb = ForbiddenEqualsRelation(hp1, hp2) - with self.assertRaises(ForbiddenValueError): - cs.add_forbidden_clause(forb) - - def test_add_forbidden_illegal(self): - cs = ConfigurationSpace() - hp = CategoricalHyperparameter("input1", [0, 1]) - forb = ForbiddenEqualsClause(hp, 1) - - with self.assertRaises(HyperparameterNotFoundError): - cs.add_forbidden_clause(forb) - - forb2 = ForbiddenEqualsClause(hp, 0) - - with self.assertRaises(HyperparameterNotFoundError): - cs.add_forbidden_clauses([forb, forb2]) - - def test_add_configuration_space(self): - cs = ConfigurationSpace() - hp1 = cs.add_hyperparameter(CategoricalHyperparameter("input1", [0, 1])) - cs.add_forbidden_clause(ForbiddenEqualsClause(hp1, 1)) - hp2 = cs.add_hyperparameter(UniformIntegerHyperparameter("child", 0, 10)) - cs.add_condition(EqualsCondition(hp2, hp1, 0)) - cs2 = ConfigurationSpace() - cs2.add_configuration_space("prefix", cs, delimiter="__") - assert ( - str(cs2) - == "Configuration space object:\n Hyperparameters:\n prefix__child, Type: UniformInteger, Range: [0, 10], Default: 5\n prefix__input1, Type: Categorical, Choices: {0, 1}, Default: 0\n Conditions:\n prefix__child | prefix__input1 == 0\n Forbidden Clauses:\n Forbidden: prefix__input1 == 1\n" - ) - - def test_add_configuration_space_conjunctions(self): - cs1 = ConfigurationSpace() - cs2 = ConfigurationSpace() - - hp1 = cs1.add_hyperparameter(CategoricalHyperparameter("input1", [0, 1])) - hp2 = cs1.add_hyperparameter(CategoricalHyperparameter("input2", [0, 1])) - hp3 = cs1.add_hyperparameter(UniformIntegerHyperparameter("child1", 0, 10)) - hp4 = cs1.add_hyperparameter(UniformIntegerHyperparameter("child2", 0, 10)) - - cond1 = EqualsCondition(hp2, hp3, 0) - cond2 = EqualsCondition(hp1, hp3, 5) - cond3 = EqualsCondition(hp1, hp4, 1) - andCond = AndConjunction(cond2, cond3) - - cs1.add_conditions([cond1, andCond]) - cs2.add_configuration_space(prefix="test", configuration_space=cs1) - - assert str(cs2).count("test:") == 10 - # Check that they're equal except for the "test:" prefix - assert str(cs1) == str(cs2).replace("test:", "") - - def test_add_conditions(self): - cs1 = ConfigurationSpace() - cs2 = ConfigurationSpace() - - hp1 = cs1.add_hyperparameter(CategoricalHyperparameter("input1", [0, 1])) - cs2.add_hyperparameter(hp1) - hp2 = cs1.add_hyperparameter(CategoricalHyperparameter("input2", [0, 1])) - cs2.add_hyperparameter(hp2) - hp3 = cs1.add_hyperparameter(UniformIntegerHyperparameter("child1", 0, 10)) - cs2.add_hyperparameter(hp3) - hp4 = cs1.add_hyperparameter(UniformIntegerHyperparameter("child2", 0, 10)) - cs2.add_hyperparameter(hp4) - - cond1 = EqualsCondition(hp2, hp3, 0) - cond2 = EqualsCondition(hp1, hp3, 5) - cond3 = EqualsCondition(hp1, hp4, 1) - andCond = AndConjunction(cond2, cond3) - - cs1.add_conditions([cond1, andCond]) - cs2.add_condition(cond1) - cs2.add_condition(andCond) - - assert str(cs1) == str(cs2) - - def test_get_hyperparamforbidden_clauseseters(self): - cs = ConfigurationSpace() - assert len(cs) == 0 - hp1 = CategoricalHyperparameter("parent", [0, 1]) - cs.add_hyperparameter(hp1) - assert [hp1] == list(cs.values()) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cs.add_hyperparameter(hp2) - cond1 = EqualsCondition(hp2, hp1, 1) - cs.add_condition(cond1) - assert [hp1, hp2] == list(cs.values()) - # TODO: I need more tests for the topological sort! - assert [hp1, hp2] == list(cs.values()) - - def test_get_hyperparameters_topological_sort_simple(self): - for _ in range(10): - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("parent", [0, 1]) - cs.add_hyperparameter(hp1) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cs.add_hyperparameter(hp2) - cond1 = EqualsCondition(hp2, hp1, 0) - cs.add_condition(cond1) - # This automatically checks the configuration! - Configuration(cs, {"parent": 0, "child": 5}) - - def test_get_hyperparameters_topological_sort(self): - # and now for something more complicated - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("input1", [0, 1]) - hp2 = CategoricalHyperparameter("input2", [0, 1]) - hp3 = CategoricalHyperparameter("input3", [0, 1]) - hp4 = CategoricalHyperparameter("input4", [0, 1]) - hp5 = CategoricalHyperparameter("input5", [0, 1]) - hp6 = Constant("AND", "True") - # More top-level hyperparameters - hp7 = CategoricalHyperparameter("input7", [0, 1]) - # Somewhat shuffled - hyperparameters = [hp1, hp2, hp3, hp4, hp5, hp6, hp7] - - for hp in hyperparameters: - cs.add_hyperparameter(hp) - - cond1 = EqualsCondition(hp6, hp1, 1) - cond2 = NotEqualsCondition(hp6, hp2, 1) - cond3 = InCondition(hp6, hp3, [1]) - cond4 = EqualsCondition(hp5, hp3, 1) - cond5 = EqualsCondition(hp4, hp5, 1) - cond6 = EqualsCondition(hp6, hp4, 1) - cond7 = EqualsCondition(hp6, hp5, 1) - - conj1 = AndConjunction(cond1, cond2) - conj2 = OrConjunction(conj1, cond3) - conj3 = AndConjunction(conj2, cond6, cond7) - - cs.add_condition(cond4) - hps = list(cs.values()) - # AND is moved to the front because of alphabetical sorting - for hp, idx in zip(hyperparameters, [1, 2, 3, 4, 6, 0, 5]): - assert hps.index(hp) == idx - assert cs._hyperparameter_idx[hp.name] == idx - assert cs._idx_to_hyperparameter[idx] == hp.name - - cs.add_condition(cond5) - hps = list(cs.values()) - for hp, idx in zip(hyperparameters, [1, 2, 3, 6, 5, 0, 4]): - assert hps.index(hp) == idx - assert cs._hyperparameter_idx[hp.name] == idx - assert cs._idx_to_hyperparameter[idx] == hp.name - - cs.add_condition(conj3) - hps = list(cs.values()) - for hp, idx in zip(hyperparameters, [0, 1, 2, 5, 4, 6, 3]): - assert hps.index(hp) == idx - assert cs._hyperparameter_idx[hp.name] == idx - assert cs._idx_to_hyperparameter[idx] == hp.name - - def test_get_hyperparameter(self): - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("parent", [0, 1]) - cs.add_hyperparameter(hp1) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cs.add_hyperparameter(hp2) - retval = cs["parent"] - assert hp1 == retval - retval = cs["child"] - assert hp2 == retval - with self.assertRaises(HyperparameterNotFoundError): - cs["grandfather"] +def test_add_forbidden_illegal(): + cs = ConfigurationSpace() + hp = CategoricalHyperparameter("input1", [0, 1]) + forb = ForbiddenEqualsClause(hp, 1) - def test_get_conditions(self): - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("parent", [0, 1]) - cs.add_hyperparameter(hp1) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cs.add_hyperparameter(hp2) - assert [] == cs.get_conditions() - cond1 = EqualsCondition(hp2, hp1, 0) - cs.add_condition(cond1) - assert [cond1] == cs.get_conditions() - - def test_get_parent_and_chil_conditions_of(self): - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("parent", [0, 1]) - cs.add_hyperparameter(hp1) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cs.add_hyperparameter(hp2) - cond1 = EqualsCondition(hp2, hp1, 0) - cs.add_condition(cond1) - - assert [cond1] == cs.get_parent_conditions_of(hp2.name) - assert [cond1] == cs.get_parent_conditions_of(hp2) - assert [cond1] == cs.get_child_conditions_of(hp1.name) - assert [cond1] == cs.get_child_conditions_of(hp1) - - with self.assertRaises(HyperparameterNotFoundError): - cs.get_parents_of("Foo") - - with self.assertRaises(HyperparameterNotFoundError): - cs.get_children_of("Foo") - - def test_get_parent_and_children_of(self): - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("parent", [0, 1]) - cs.add_hyperparameter(hp1) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cs.add_hyperparameter(hp2) - cond1 = EqualsCondition(hp2, hp1, 0) - cs.add_condition(cond1) - - assert [hp1] == cs.get_parents_of(hp2.name) - assert [hp1] == cs.get_parents_of(hp2) - assert [hp2] == cs.get_children_of(hp1.name) - assert [hp2] == cs.get_children_of(hp1) - - with self.assertRaises(HyperparameterNotFoundError): - cs.get_parents_of("Foo") - - with self.assertRaises(HyperparameterNotFoundError): - cs.get_children_of("Foo") - - def test_check_configuration_input_checking(self): - cs = ConfigurationSpace() - with self.assertRaises(TypeError): - cs.check_configuration("String") # type: ignore - - with self.assertRaises(TypeError): - cs.check_configuration_vector_representation("String") # type: ignore + with pytest.raises(HyperparameterNotFoundError): + cs.add_forbidden_clause(forb) - def test_check_configuration(self): - # TODO this is only a smoke test - # TODO actually, this rather tests the evaluate methods in the - # conditions module! + forb2 = ForbiddenEqualsClause(hp, 0) + + with pytest.raises(HyperparameterNotFoundError): + cs.add_forbidden_clauses([forb, forb2]) + + +def test_add_configuration_space(): + cs = ConfigurationSpace() + hp1 = cs.add_hyperparameter(CategoricalHyperparameter("input1", [0, 1])) + cs.add_forbidden_clause(ForbiddenEqualsClause(hp1, 1)) + hp2 = cs.add_hyperparameter(UniformIntegerHyperparameter("child", 0, 10)) + cs.add_condition(EqualsCondition(hp2, hp1, 0)) + cs2 = ConfigurationSpace() + cs2.add_configuration_space("prefix", cs, delimiter="__") + assert ( + str(cs2) + == "Configuration space object:\n Hyperparameters:\n prefix__child, Type: UniformInteger, Range: [0, 10], Default: 5\n prefix__input1, Type: Categorical, Choices: {0, 1}, Default: 0\n Conditions:\n prefix__child | prefix__input1 == 0\n Forbidden Clauses:\n Forbidden: prefix__input1 == 1\n" + ) + + +def test_add_configuration_space_conjunctions(): + cs1 = ConfigurationSpace() + cs2 = ConfigurationSpace() + + hp1 = cs1.add_hyperparameter(CategoricalHyperparameter("input1", [0, 1])) + hp2 = cs1.add_hyperparameter(CategoricalHyperparameter("input2", [0, 1])) + hp3 = cs1.add_hyperparameter(UniformIntegerHyperparameter("child1", 0, 10)) + hp4 = cs1.add_hyperparameter(UniformIntegerHyperparameter("child2", 0, 10)) + + cond1 = EqualsCondition(hp2, hp3, 0) + cond2 = EqualsCondition(hp1, hp3, 5) + cond3 = EqualsCondition(hp1, hp4, 1) + andCond = AndConjunction(cond2, cond3) + + cs1.add_conditions([cond1, andCond]) + cs2.add_configuration_space(prefix="test", configuration_space=cs1) + + assert str(cs2).count("test:") == 10 + # Check that they're equal except for the "test:" prefix + assert str(cs1) == str(cs2).replace("test:", "") + + +def test_add_conditions(): + cs1 = ConfigurationSpace() + cs2 = ConfigurationSpace() + + hp1 = cs1.add_hyperparameter(CategoricalHyperparameter("input1", [0, 1])) + cs2.add_hyperparameter(hp1) + hp2 = cs1.add_hyperparameter(CategoricalHyperparameter("input2", [0, 1])) + cs2.add_hyperparameter(hp2) + hp3 = cs1.add_hyperparameter(UniformIntegerHyperparameter("child1", 0, 10)) + cs2.add_hyperparameter(hp3) + hp4 = cs1.add_hyperparameter(UniformIntegerHyperparameter("child2", 0, 10)) + cs2.add_hyperparameter(hp4) + + cond1 = EqualsCondition(hp2, hp3, 0) + cond2 = EqualsCondition(hp1, hp3, 5) + cond3 = EqualsCondition(hp1, hp4, 1) + andCond = AndConjunction(cond2, cond3) + + cs1.add_conditions([cond1, andCond]) + cs2.add_condition(cond1) + cs2.add_condition(andCond) + + assert str(cs1) == str(cs2) + + +def test_get_hyperparamforbidden_clauseseters(): + cs = ConfigurationSpace() + assert len(cs) == 0 + hp1 = CategoricalHyperparameter("parent", [0, 1]) + cs.add_hyperparameter(hp1) + assert [hp1] == list(cs.values()) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cs.add_hyperparameter(hp2) + cond1 = EqualsCondition(hp2, hp1, 1) + cs.add_condition(cond1) + assert [hp1, hp2] == list(cs.values()) + # TODO: I need more tests for the topological sort! + assert [hp1, hp2] == list(cs.values()) + + +def test_get_hyperparameters_topological_sort_simple(): + for _ in range(10): cs = ConfigurationSpace() hp1 = CategoricalHyperparameter("parent", [0, 1]) cs.add_hyperparameter(hp1) @@ -481,92 +357,224 @@ def test_check_configuration(self): # This automatically checks the configuration! Configuration(cs, {"parent": 0, "child": 5}) - # and now for something more complicated - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("input1", [0, 1]) - cs.add_hyperparameter(hp1) - hp2 = CategoricalHyperparameter("input2", [0, 1]) - cs.add_hyperparameter(hp2) - hp3 = CategoricalHyperparameter("input3", [0, 1]) - cs.add_hyperparameter(hp3) - hp4 = CategoricalHyperparameter("input4", [0, 1]) - cs.add_hyperparameter(hp4) - hp5 = CategoricalHyperparameter("input5", [0, 1]) - cs.add_hyperparameter(hp5) - hp6 = Constant("AND", "True") - cs.add_hyperparameter(hp6) - - cond1 = EqualsCondition(hp6, hp1, 1) - cond2 = NotEqualsCondition(hp6, hp2, 1) - cond3 = InCondition(hp6, hp3, [1]) - cond4 = EqualsCondition(hp6, hp4, 1) - cond5 = EqualsCondition(hp6, hp5, 1) - - conj1 = AndConjunction(cond1, cond2) - conj2 = OrConjunction(conj1, cond3) - conj3 = AndConjunction(conj2, cond4, cond5) - cs.add_condition(conj3) - - expected_outcomes = [ - False, - False, - False, - False, - False, - False, - False, - True, - False, - False, - False, - False, - False, - False, - False, - True, - False, - False, - False, - True, - False, - False, - False, - True, - False, - False, - False, - False, - False, - False, - False, - True, - ] - - for idx, values in enumerate(product([0, 1], repeat=5)): - # The hyperparameters aren't sorted, but the test assumes them to - # be sorted. - hyperparameters = sorted(cs.values(), key=lambda t: t.name) - instantiations = { - hyperparameters[jdx + 1].name: values[jdx] for jdx in range(len(values)) - } - - evaluation = conj3.evaluate(instantiations) - assert expected_outcomes[idx] == evaluation - - if not evaluation: - with self.assertRaises(InactiveHyperparameterSetError): - Configuration( - cs, - values={ - "input1": values[0], - "input2": values[1], - "input3": values[2], - "input4": values[3], - "input5": values[4], - "AND": "True", - }, - ) - else: + +def test_get_hyperparameters_topological_sort(): + # and now for something more complicated + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("input1", [0, 1]) + hp2 = CategoricalHyperparameter("input2", [0, 1]) + hp3 = CategoricalHyperparameter("input3", [0, 1]) + hp4 = CategoricalHyperparameter("input4", [0, 1]) + hp5 = CategoricalHyperparameter("input5", [0, 1]) + hp6 = Constant("AND", "True") + # More top-level hyperparameters + hp7 = CategoricalHyperparameter("input7", [0, 1]) + # Somewhat shuffled + hyperparameters = [hp1, hp2, hp3, hp4, hp5, hp6, hp7] + + for hp in hyperparameters: + cs.add_hyperparameter(hp) + + cond1 = EqualsCondition(hp6, hp1, 1) + cond2 = NotEqualsCondition(hp6, hp2, 1) + cond3 = InCondition(hp6, hp3, [1]) + cond4 = EqualsCondition(hp5, hp3, 1) + cond5 = EqualsCondition(hp4, hp5, 1) + cond6 = EqualsCondition(hp6, hp4, 1) + cond7 = EqualsCondition(hp6, hp5, 1) + + conj1 = AndConjunction(cond1, cond2) + conj2 = OrConjunction(conj1, cond3) + conj3 = AndConjunction(conj2, cond6, cond7) + + cs.add_condition(cond4) + hps = list(cs.values()) + # AND is moved to the front because of alphabetical sorting + for hp, idx in zip(hyperparameters, [1, 2, 3, 4, 6, 0, 5]): + assert hps.index(hp) == idx + assert cs._hyperparameter_idx[hp.name] == idx + assert cs._idx_to_hyperparameter[idx] == hp.name + + cs.add_condition(cond5) + hps = list(cs.values()) + for hp, idx in zip(hyperparameters, [1, 2, 3, 6, 5, 0, 4]): + assert hps.index(hp) == idx + assert cs._hyperparameter_idx[hp.name] == idx + assert cs._idx_to_hyperparameter[idx] == hp.name + + cs.add_condition(conj3) + hps = list(cs.values()) + for hp, idx in zip(hyperparameters, [0, 1, 2, 5, 4, 6, 3]): + assert hps.index(hp) == idx + assert cs._hyperparameter_idx[hp.name] == idx + assert cs._idx_to_hyperparameter[idx] == hp.name + + +def test_get_hyperparameter(): + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("parent", [0, 1]) + cs.add_hyperparameter(hp1) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cs.add_hyperparameter(hp2) + + retval = cs["parent"] + assert hp1 == retval + retval = cs["child"] + assert hp2 == retval + + with pytest.raises(HyperparameterNotFoundError): + cs["grandfather"] + + +def test_get_conditions(): + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("parent", [0, 1]) + cs.add_hyperparameter(hp1) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cs.add_hyperparameter(hp2) + assert [] == cs.get_conditions() + cond1 = EqualsCondition(hp2, hp1, 0) + cs.add_condition(cond1) + assert [cond1] == cs.get_conditions() + + +def test_get_parent_and_chil_conditions_of(): + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("parent", [0, 1]) + cs.add_hyperparameter(hp1) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cs.add_hyperparameter(hp2) + cond1 = EqualsCondition(hp2, hp1, 0) + cs.add_condition(cond1) + + assert [cond1] == cs.get_parent_conditions_of(hp2.name) + assert [cond1] == cs.get_parent_conditions_of(hp2) + assert [cond1] == cs.get_child_conditions_of(hp1.name) + assert [cond1] == cs.get_child_conditions_of(hp1) + + with pytest.raises(HyperparameterNotFoundError): + cs.get_parents_of("Foo") + + with pytest.raises(HyperparameterNotFoundError): + cs.get_children_of("Foo") + + +def test_get_parent_and_children_of(): + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("parent", [0, 1]) + cs.add_hyperparameter(hp1) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cs.add_hyperparameter(hp2) + cond1 = EqualsCondition(hp2, hp1, 0) + cs.add_condition(cond1) + + assert [hp1] == cs.get_parents_of(hp2.name) + assert [hp1] == cs.get_parents_of(hp2) + assert [hp2] == cs.get_children_of(hp1.name) + assert [hp2] == cs.get_children_of(hp1) + + with pytest.raises(HyperparameterNotFoundError): + cs.get_parents_of("Foo") + + with pytest.raises(HyperparameterNotFoundError): + cs.get_children_of("Foo") + + +def test_check_configuration_input_checking(): + cs = ConfigurationSpace() + with pytest.raises(TypeError): + cs.check_configuration("String") # type: ignore + + with pytest.raises(TypeError): + cs.check_configuration_vector_representation("String") # type: ignore + + +def test_check_configuration(): + # TODO this is only a smoke test + # TODO actually, this rather tests the evaluate methods in the + # conditions module! + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("parent", [0, 1]) + cs.add_hyperparameter(hp1) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cs.add_hyperparameter(hp2) + cond1 = EqualsCondition(hp2, hp1, 0) + cs.add_condition(cond1) + # This automatically checks the configuration! + Configuration(cs, {"parent": 0, "child": 5}) + + # and now for something more complicated + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("input1", [0, 1]) + cs.add_hyperparameter(hp1) + hp2 = CategoricalHyperparameter("input2", [0, 1]) + cs.add_hyperparameter(hp2) + hp3 = CategoricalHyperparameter("input3", [0, 1]) + cs.add_hyperparameter(hp3) + hp4 = CategoricalHyperparameter("input4", [0, 1]) + cs.add_hyperparameter(hp4) + hp5 = CategoricalHyperparameter("input5", [0, 1]) + cs.add_hyperparameter(hp5) + hp6 = Constant("AND", "True") + cs.add_hyperparameter(hp6) + + cond1 = EqualsCondition(hp6, hp1, 1) + cond2 = NotEqualsCondition(hp6, hp2, 1) + cond3 = InCondition(hp6, hp3, [1]) + cond4 = EqualsCondition(hp6, hp4, 1) + cond5 = EqualsCondition(hp6, hp5, 1) + + conj1 = AndConjunction(cond1, cond2) + conj2 = OrConjunction(conj1, cond3) + conj3 = AndConjunction(conj2, cond4, cond5) + cs.add_condition(conj3) + + expected_outcomes = [ + False, + False, + False, + False, + False, + False, + False, + True, + False, + False, + False, + False, + False, + False, + False, + True, + False, + False, + False, + True, + False, + False, + False, + True, + False, + False, + False, + False, + False, + False, + False, + True, + ] + + for idx, values in enumerate(product([0, 1], repeat=5)): + # The hyperparameters aren't sorted, but the test assumes them to + # be sorted. + hyperparameters = sorted(cs.values(), key=lambda t: t.name) + instantiations = {hyperparameters[jdx + 1].name: values[jdx] for jdx in range(len(values))} + + evaluation = conj3.evaluate(instantiations) + assert expected_outcomes[idx] == evaluation + + if not evaluation: + with pytest.raises(InactiveHyperparameterSetError): Configuration( cs, values={ @@ -578,686 +586,718 @@ def test_check_configuration(self): "AND": "True", }, ) - - def test_check_configuration2(self): - # Test that hyperparameters which are not active must not be set and - # that evaluating forbidden clauses does not choke on missing - # hyperparameters - cs = ConfigurationSpace() - classifier = CategoricalHyperparameter("classifier", ["k_nearest_neighbors", "extra_trees"]) - metric = CategoricalHyperparameter("metric", ["minkowski", "other"]) - p = CategoricalHyperparameter("k_nearest_neighbors:p", [1, 2]) - metric_depends_on_classifier = EqualsCondition(metric, classifier, "k_nearest_neighbors") - p_depends_on_metric = EqualsCondition(p, metric, "minkowski") - cs.add_hyperparameter(metric) - cs.add_hyperparameter(p) - cs.add_hyperparameter(classifier) - cs.add_condition(metric_depends_on_classifier) - cs.add_condition(p_depends_on_metric) - - forbidden = ForbiddenEqualsClause(metric, "other") - cs.add_forbidden_clause(forbidden) - - configuration = Configuration(cs, {"classifier": "extra_trees"}) - - # check backward compatibility with checking configurations instead of vectors - cs.check_configuration(configuration) - - def test_check_forbidden_with_sampled_vector_configuration(self): - cs = ConfigurationSpace() - metric = CategoricalHyperparameter("metric", ["minkowski", "other"]) - cs.add_hyperparameter(metric) - - forbidden = ForbiddenEqualsClause(metric, "other") - cs.add_forbidden_clause(forbidden) - configuration = Configuration(cs, vector=np.ones(1, dtype=float)) - - with self.assertRaisesRegex(ValueError, "violates forbidden clause"): - cs._check_forbidden(configuration.get_array()) - - def test_eq(self): - # Compare empty configuration spaces - cs1 = ConfigurationSpace() - cs2 = ConfigurationSpace() - assert cs1 == cs2 - - # Compare to something which isn't a configuration space - assert cs1 != "ConfigurationSpace" - - # Compare to equal configuration spaces - hp1 = CategoricalHyperparameter("parent", [0, 1]) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - hp3 = UniformIntegerHyperparameter("friend", 0, 5) - cond1 = EqualsCondition(hp2, hp1, 0) - cs1.add_hyperparameter(hp1) - cs1.add_hyperparameter(hp2) - cs1.add_condition(cond1) - cs2.add_hyperparameter(hp1) - cs2.add_hyperparameter(hp2) - cs2.add_condition(cond1) - assert cs1 == cs2 - cs1.add_hyperparameter(hp3) - assert cs1 != cs2 - - def test_neq(self): - cs1 = ConfigurationSpace() - assert cs1 != "ConfigurationSpace" - - def test_repr(self): - cs1 = ConfigurationSpace() - retval = cs1.__str__() - assert retval == "Configuration space object:\n Hyperparameters:\n" - - hp1 = CategoricalHyperparameter("parent", [0, 1]) - cs1.add_hyperparameter(hp1) - retval = cs1.__str__() - assert "Configuration space object:\n Hyperparameters:\n %s\n" % str(hp1) == retval - - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cond1 = EqualsCondition(hp2, hp1, 0) - cs1.add_hyperparameter(hp2) - cs1.add_condition(cond1) - retval = cs1.__str__() - assert ( - f"Configuration space object:\n Hyperparameters:\n {str(hp2)}\n {str(hp1)}\n Conditions:\n {str(cond1)}\n" - == retval - ) - - def test_sample_configuration(self): - cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter("parent", [0, 1]) - cs.add_hyperparameter(hp1) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - cs.add_hyperparameter(hp2) - cond1 = EqualsCondition(hp2, hp1, 0) - cs.add_condition(cond1) - # This automatically checks the configuration! - Configuration(cs, {"parent": 0, "child": 5}) - - # and now for something more complicated - cs = ConfigurationSpace(seed=1) - hp1 = CategoricalHyperparameter("input1", [0, 1]) - cs.add_hyperparameter(hp1) - hp2 = CategoricalHyperparameter("input2", [0, 1]) - cs.add_hyperparameter(hp2) - hp3 = CategoricalHyperparameter("input3", [0, 1]) - cs.add_hyperparameter(hp3) - hp4 = CategoricalHyperparameter("input4", [0, 1]) - cs.add_hyperparameter(hp4) - hp5 = CategoricalHyperparameter("input5", [0, 1]) - cs.add_hyperparameter(hp5) - hp6 = Constant("AND", "True") - cs.add_hyperparameter(hp6) - - cond1 = EqualsCondition(hp6, hp1, 1) - cond2 = NotEqualsCondition(hp6, hp2, 1) - cond3 = InCondition(hp6, hp3, [1]) - cond4 = EqualsCondition(hp5, hp3, 1) - cond5 = EqualsCondition(hp4, hp5, 1) - cond6 = EqualsCondition(hp6, hp4, 1) - cond7 = EqualsCondition(hp6, hp5, 1) - - conj1 = AndConjunction(cond1, cond2) - conj2 = OrConjunction(conj1, cond3) - conj3 = AndConjunction(conj2, cond6, cond7) - cs.add_condition(cond4) - cs.add_condition(cond5) - cs.add_condition(conj3) - - samples: list[list[Configuration]] = [] - for i in range(5): - cs.seed(1) - samples.append([]) - for _ in range(100): - sample = cs.sample_configuration() - samples[-1].append(sample) - - if i > 0: - for j in range(100): - assert samples[-1][j] == samples[-2][j] - - def test_sample_configuration_with_or_conjunction(self): - cs = ConfigurationSpace(seed=1) - - hyper_params = {} - hyper_params["hp5"] = CategoricalHyperparameter("hp5", ["0", "1", "2"]) - hyper_params["hp7"] = CategoricalHyperparameter("hp7", ["3", "4", "5"]) - hyper_params["hp8"] = CategoricalHyperparameter("hp8", ["6", "7", "8"]) - for key in hyper_params: - cs.add_hyperparameter(hyper_params[key]) - - cs.add_condition(InCondition(hyper_params["hp5"], hyper_params["hp8"], ["6"])) - - cs.add_condition( - OrConjunction( - InCondition(hyper_params["hp7"], hyper_params["hp8"], ["7"]), - InCondition(hyper_params["hp7"], hyper_params["hp5"], ["1"]), - ), - ) - - for cfg, fixture in zip( - cs.sample_configuration(10), - [ - [1, np.NaN, 2], - [2, np.NaN, np.NaN], - [0, 0, np.NaN], - [0, 2, np.NaN], - [0, 0, np.NaN], - ], - ): - np.testing.assert_array_almost_equal(cfg.get_array(), fixture) - - def test_sample_wrong_argument(self): - cs = ConfigurationSpace() - with self.assertRaises(TypeError): - cs.sample_configuration(1.2) # type: ignore - - def test_sample_no_configuration(self): - cs = ConfigurationSpace() - rval = cs.sample_configuration(size=0) - assert len(rval) == 0 - - def test_subspace_switches(self): - # create a switch to select one of two algorithms - algo_switch = CategoricalHyperparameter( - name="switch", - choices=["algo1", "algo2"], - weights=[0.25, 0.75], - default_value="algo1", - ) - - # create sub-configuration space for algorithm 1 - algo1_cs = ConfigurationSpace() - hp1 = CategoricalHyperparameter( - name="algo1_param1", - choices=["A", "B"], - weights=[0.3, 0.7], - default_value="B", - ) - algo1_cs.add_hyperparameter(hp1) - - # create sub-configuration space for algorithm 2 - algo2_cs = ConfigurationSpace() - hp2 = CategoricalHyperparameter(name="algo2_param1", choices=["X", "Y"], default_value="Y") - algo2_cs.add_hyperparameter(hp2) - - # create a configuration space and populate it with both the switch - # and the two sub-configuration spaces - cs = ConfigurationSpace() - cs.add_hyperparameter(algo_switch) - cs.add_configuration_space( - prefix="algo1_subspace", - configuration_space=algo1_cs, - parent_hyperparameter={"parent": algo_switch, "value": "algo1"}, - ) - cs.add_configuration_space( - prefix="algo2_subspace", - configuration_space=algo2_cs, - parent_hyperparameter={"parent": algo_switch, "value": "algo2"}, - ) - - # check choices in the final configuration space - assert cs["switch"].choices == ("algo1", "algo2") - assert cs["algo1_subspace:algo1_param1"].choices == ("A", "B") - assert cs["algo2_subspace:algo2_param1"].choices == ("X", "Y") - - # check probabilities in the final configuration space - assert cs["switch"].probabilities == (0.25, 0.75) - assert cs["algo1_subspace:algo1_param1"].probabilities == (0.3, 0.7) - self.assertTupleEqual( - (0.5, 0.5), - cs["algo2_subspace:algo2_param1"].probabilities, - ) - - # check default values in the final configuration space - assert cs["switch"].default_value == "algo1" - assert cs["algo1_subspace:algo1_param1"].default_value == "B" - assert cs["algo2_subspace:algo2_param1"].default_value == "Y" - - def test_acts_as_mapping(self): - """ - Test that ConfigurationSpace can act as a mapping with iteration, - indexing and items, values, keys. - """ - cs = ConfigurationSpace() - names = [f"name{i}" for i in range(5)] - hyperparameters = [UniformIntegerHyperparameter(name, 0, 10) for name in names] - cs.add_hyperparameters(hyperparameters) - - # Test indexing - assert cs["name3"] == hyperparameters[3] - - # Test dict methods - assert list(cs.keys()) == names - assert list(cs.values()) == hyperparameters - assert list(cs.items()) == list(zip(names, hyperparameters)) - assert len(cs) == 5 - - # Test __iter__ - assert list(iter(cs)) == names - - # Test unpacking - d = {**cs} - assert list(d.keys()) == names - assert list(d.values()) == hyperparameters - assert list(d.items()) == list(zip(names, hyperparameters)) - assert len(d) == 5 - - def test_remove_hyperparameter_priors(self): - cs = ConfigurationSpace() - integer = UniformIntegerHyperparameter("integer", 1, 5, log=True) - cat = CategoricalHyperparameter("cat", [0, 1, 2], weights=[1, 2, 3]) - beta = BetaFloatHyperparameter("beta", alpha=8, beta=2, lower=-1, upper=11) - norm = NormalIntegerHyperparameter("norm", mu=5, sigma=4, lower=1, upper=15) - cs.add_hyperparameters([integer, cat, beta, norm]) - cat_default = cat.default_value - norm_default = norm.default_value - beta_default = beta.default_value - - # add some conditions, to test that remove_parameter_priors keeps the forbiddens - cond_1 = EqualsCondition(norm, cat, 2) - cond_2 = OrConjunction(EqualsCondition(beta, cat, 0), EqualsCondition(beta, cat, 1)) - cond_3 = OrConjunction( - EqualsCondition(norm, integer, 1), - EqualsCondition(norm, integer, 3), - EqualsCondition(norm, integer, 5), - ) - cs.add_conditions([cond_1, cond_2, cond_3]) - - # add some forbidden clauses too, to test that remove_parameter_priors keeps the forbiddens - forbidden_clause_a = ForbiddenEqualsClause(cat, 0) - forbidden_clause_c = ForbiddenEqualsClause(integer, 3) - forbidden_clause_d = ForbiddenAndConjunction(forbidden_clause_a, forbidden_clause_c) - cs.add_forbidden_clauses([forbidden_clause_c, forbidden_clause_d]) - uniform_cs = cs.remove_hyperparameter_priors() - - expected_cs = ConfigurationSpace() - unif_integer = UniformIntegerHyperparameter("integer", 1, 5, log=True) - unif_cat = CategoricalHyperparameter("cat", [0, 1, 2], default_value=cat_default) - - unif_beta = UniformFloatHyperparameter( - "beta", - lower=-1, - upper=11, - default_value=beta_default, - ) - unif_norm = UniformIntegerHyperparameter( - "norm", - lower=1, - upper=15, - default_value=norm_default, - ) - expected_cs.add_hyperparameters([unif_integer, unif_cat, unif_beta, unif_norm]) - - # add some conditions, to test that remove_parameter_priors keeps the forbiddens - cond_1 = EqualsCondition(unif_norm, unif_cat, 2) - cond_2 = OrConjunction( - EqualsCondition(unif_beta, unif_cat, 0), - EqualsCondition(unif_beta, unif_cat, 1), - ) - cond_3 = OrConjunction( - EqualsCondition(unif_norm, unif_integer, 1), - EqualsCondition(unif_norm, unif_integer, 3), - EqualsCondition(unif_norm, unif_integer, 5), - ) - expected_cs.add_conditions([cond_1, cond_2, cond_3]) - - # add some forbidden clauses too, to test that remove_parameter_priors keeps the forbiddens - forbidden_clause_a = ForbiddenEqualsClause(unif_cat, 0) - forbidden_clause_c = ForbiddenEqualsClause(unif_integer, 3) - forbidden_clause_d = ForbiddenAndConjunction(forbidden_clause_a, forbidden_clause_c) - expected_cs.add_forbidden_clauses([forbidden_clause_c, forbidden_clause_d]) - - # __eq__ not implemented, so this is the next best thing - assert repr(uniform_cs) == repr(expected_cs) - - def test_substitute_hyperparameters_in_conditions(self): - cs1 = ConfigurationSpace() - orig_hp1 = CategoricalHyperparameter("input1", [0, 1]) - orig_hp2 = CategoricalHyperparameter("input2", [0, 1]) - orig_hp3 = UniformIntegerHyperparameter("child1", 0, 10) - orig_hp4 = UniformIntegerHyperparameter("child2", 0, 10) - cs1.add_hyperparameters([orig_hp1, orig_hp2, orig_hp3, orig_hp4]) - cond1 = EqualsCondition(orig_hp2, orig_hp3, 0) - cond2 = EqualsCondition(orig_hp1, orig_hp3, 5) - cond3 = EqualsCondition(orig_hp1, orig_hp4, 1) - andCond = AndConjunction(cond2, cond3) - cs1.add_conditions([cond1, andCond]) - - cs2 = ConfigurationSpace() - sub_hp1 = CategoricalHyperparameter("input1", [0, 1, 2]) - sub_hp2 = CategoricalHyperparameter("input2", [0, 1, 3]) - sub_hp3 = NormalIntegerHyperparameter("child1", lower=0, upper=10, mu=5, sigma=2) - sub_hp4 = BetaIntegerHyperparameter("child2", lower=0, upper=10, alpha=3, beta=5) - cs2.add_hyperparameters([sub_hp1, sub_hp2, sub_hp3, sub_hp4]) - new_conditions = cs1.substitute_hyperparameters_in_conditions(cs1.get_conditions(), cs2) - - test_cond1 = EqualsCondition(sub_hp2, sub_hp3, 0) - test_cond2 = EqualsCondition(sub_hp1, sub_hp3, 5) - test_cond3 = EqualsCondition(sub_hp1, sub_hp4, 1) - test_andCond = AndConjunction(test_cond2, test_cond3) - cs2.add_conditions([test_cond1, test_andCond]) - test_conditions = cs2.get_conditions() - - assert new_conditions[0] == test_conditions[0] - assert new_conditions[1] == test_conditions[1] - - def test_substitute_hyperparameters_in_inconditions(self): - cs1 = ConfigurationSpace() - a = UniformIntegerHyperparameter("a", lower=0, upper=10) - b = UniformFloatHyperparameter("b", lower=1.0, upper=8.0, log=False) - cs1.add_hyperparameters([a, b]) - - cond = InCondition(b, a, [1, 2, 3, 4]) - cs1.add_conditions([cond]) - - cs2 = ConfigurationSpace() - sub_a = UniformIntegerHyperparameter("a", lower=0, upper=10) - sub_b = UniformFloatHyperparameter("b", lower=1.0, upper=8.0, log=False) - cs2.add_hyperparameters([sub_a, sub_b]) - new_conditions = cs1.substitute_hyperparameters_in_conditions(cs1.get_conditions(), cs2) - - test_cond = InCondition(b, a, [1, 2, 3, 4]) - cs2.add_conditions([test_cond]) - test_conditions = cs2.get_conditions() - - assert new_conditions[0] == test_conditions[0] - assert new_conditions[0] is not test_conditions[0] - - assert new_conditions[0].get_parents() == test_conditions[0].get_parents() - assert new_conditions[0].get_parents() is not test_conditions[0].get_parents() - - assert new_conditions[0].get_children() == test_conditions[0].get_children() - assert new_conditions[0].get_children() is not test_conditions[0].get_children() - - def test_substitute_hyperparameters_in_forbiddens(self): - cs1 = ConfigurationSpace() - orig_hp1 = CategoricalHyperparameter("input1", [0, 1]) - orig_hp2 = CategoricalHyperparameter("input2", [0, 1]) - orig_hp3 = UniformIntegerHyperparameter("input3", 0, 10) - orig_hp4 = UniformIntegerHyperparameter("input4", 0, 10) - cs1.add_hyperparameters([orig_hp1, orig_hp2, orig_hp3, orig_hp4]) - forb_1 = ForbiddenEqualsClause(orig_hp1, 0) - forb_2 = ForbiddenEqualsClause(orig_hp2, 1) - forb_3 = ForbiddenEqualsClause(orig_hp3, 10) - forb_4 = ForbiddenAndConjunction(forb_1, forb_2) - forb_5 = ForbiddenLessThanRelation(orig_hp1, orig_hp2) - cs1.add_forbidden_clauses([forb_3, forb_4, forb_5]) - - cs2 = ConfigurationSpace() - sub_hp1 = CategoricalHyperparameter("input1", [0, 1, 2]) - sub_hp2 = CategoricalHyperparameter("input2", [0, 1, 3]) - sub_hp3 = NormalIntegerHyperparameter("input3", lower=0, upper=10, mu=5, sigma=2) - sub_hp4 = BetaIntegerHyperparameter("input4", lower=0, upper=10, alpha=3, beta=5) - cs2.add_hyperparameters([sub_hp1, sub_hp2, sub_hp3, sub_hp4]) - new_forbiddens = cs1.substitute_hyperparameters_in_forbiddens(cs1.get_forbiddens(), cs2) - - test_forb_1 = ForbiddenEqualsClause(sub_hp1, 0) - test_forb_2 = ForbiddenEqualsClause(sub_hp2, 1) - test_forb_3 = ForbiddenEqualsClause(sub_hp3, 10) - test_forb_4 = ForbiddenAndConjunction(test_forb_1, test_forb_2) - test_forb_5 = ForbiddenLessThanRelation(sub_hp1, sub_hp2) - cs2.add_forbidden_clauses([test_forb_3, test_forb_4, test_forb_5]) - test_forbiddens = cs2.get_forbiddens() - - assert new_forbiddens[2] == test_forbiddens[2] - assert new_forbiddens[1] == test_forbiddens[1] - assert new_forbiddens[0] == test_forbiddens[0] - - def test_estimate_size(self): - cs = ConfigurationSpace() - assert cs.estimate_size() == 0 - cs.add_hyperparameter(Constant("constant", 0)) - assert cs.estimate_size() == 1 - cs.add_hyperparameter(UniformIntegerHyperparameter("integer", 0, 5)) - assert cs.estimate_size() == 6 - cs.add_hyperparameter(CategoricalHyperparameter("cat", [0, 1, 2])) - assert cs.estimate_size() == 18 - cs.add_hyperparameter(UniformFloatHyperparameter("float", 0, 1)) - assert np.isinf(cs.estimate_size()) - - -class ConfigurationTest(unittest.TestCase): - def setUp(self): - cs = ConfigurationSpace() - cs.add_hyperparameter(CategoricalHyperparameter("parent", [0, 1])) - cs.add_hyperparameter(UniformIntegerHyperparameter("child", 0, 10)) - cs.add_hyperparameter(UniformIntegerHyperparameter("friend", 0, 5)) - self.cs = cs - - def test_wrong_init(self): - with self.assertRaises(ValueError): - Configuration(self.cs) - - with self.assertRaises(ValueError): - Configuration(self.cs, values={}, vector=np.zeros((3,))) - - def test_init_with_values(self): - c1 = Configuration(self.cs, values={"parent": 1, "child": 2, "friend": 3}) - # Pay attention that the vector does not necessarily has an intuitive - # sorting! - # Values are a little bit higher than one would expect because, - # an integer range of [0,10] is transformed to [-0.499,10.499]. - vector_values = { - "parent": 1, - "child": 0.22727223140405708, - "friend": 0.583333611112037, - } - vector = [0.0] * 3 - for name in self.cs._hyperparameter_idx: - vector[self.cs._hyperparameter_idx[name]] = vector_values[name] - c2 = Configuration(self.cs, vector=vector) - # This tests - # a) that the vector representation of both are the same - # b) that the dictionary representation of both are the same - assert c1 == c2 - - def test_uniformfloat_transform(self): - """This checks whether a value sampled through the configuration - space (it does not happend when the variable is sampled alone) stays - equal when it is serialized via JSON and the deserialized again. - """ - cs = ConfigurationSpace() - a = cs.add_hyperparameter(UniformFloatHyperparameter("a", -5, 10)) - b = cs.add_hyperparameter(NormalFloatHyperparameter("b", 1, 2, log=True)) - for _i in range(100): - config = cs.sample_configuration() - value = OrderedDict(sorted(config.items())) - string = json.dumps(value) - saved_value = json.loads(string) - saved_value = OrderedDict(sorted(byteify(saved_value).items())) - assert repr(value) == repr(saved_value) - - # Next, test whether the truncation also works when initializing the - # Configuration with a dictionary - for _i in range(100): - rs = np.random.RandomState(1) - value_a = a.sample(rs) - value_b = b.sample(rs) - values_dict = {"a": value_a, "b": value_b} - config = Configuration(cs, values=values_dict) - string = json.dumps(dict(config)) - saved_value = json.loads(string) - saved_value = byteify(saved_value) - assert values_dict == saved_value - - def test_setitem(self): - """Checks overriding a sampled configuration.""" - pcs = ConfigurationSpace() - pcs.add_hyperparameter(UniformIntegerHyperparameter("x0", 1, 5, default_value=1)) - x1 = pcs.add_hyperparameter( - CategoricalHyperparameter("x1", ["ab", "bc", "cd", "de"], default_value="ab"), - ) - - # Condition - x2 = pcs.add_hyperparameter(CategoricalHyperparameter("x2", [1, 2])) - pcs.add_condition(EqualsCondition(x2, x1, "ab")) - - # Forbidden - x3 = pcs.add_hyperparameter(CategoricalHyperparameter("x3", [1, 2])) - pcs.add_forbidden_clause(ForbiddenEqualsClause(x3, 2)) - - conf = pcs.get_default_configuration() - - # failed because it's a invalid configuration - with self.assertRaises(IllegalValueError): - conf["x0"] = 0 - - # failed because the variable didn't exists - with self.assertRaises(HyperparameterNotFoundError): - conf["x_0"] = 1 - - # failed because forbidden clause is violated - with self.assertRaises(ForbiddenValueError): - conf["x3"] = 2 - - assert conf["x3"] == 1 - - # successful operation 1 - x0_old = conf["x0"] - if x0_old == 1: - conf["x0"] = 2 - else: - conf["x0"] = 1 - x0_new = conf["x0"] - assert x0_old != x0_new - pcs._check_configuration_rigorous(conf) - assert conf["x2"] == 1 - - # successful operation 2 - x1_old = conf["x1"] - if x1_old == "ab": - conf["x1"] = "cd" else: - conf["x1"] = "ab" - x1_new = conf["x1"] - assert x1_old != x1_new - pcs._check_configuration_rigorous(conf) - - with self.assertRaises(KeyError): - conf["x2"] - - def test_setting_illegal_value(self): - cs = ConfigurationSpace() - cs.add_hyperparameter(UniformFloatHyperparameter("x", 0, 1)) - configuration = {"x": 2} - with self.assertRaises(ValueError): - Configuration(cs, values=configuration) - - def test_keys(self): - # A regression test to make sure issue #49 does no longer pop up. By - # iterating over the configuration in the for loop, it should not raise - # a KeyError if the child hyperparameter is inactive. - cs = ConfigurationSpace() - shrinkage = CategoricalHyperparameter( - "shrinkage", - ["None", "auto", "manual"], - default_value="None", - ) - shrinkage_factor = UniformFloatHyperparameter( - "shrinkage_factor", - 0.0, - 1.0, - 0.5, - ) - cs.add_hyperparameters([shrinkage, shrinkage_factor]) - - cs.add_condition(EqualsCondition(shrinkage_factor, shrinkage, "manual")) - - for _ in range(10): - config = cs.sample_configuration() - {hp_name: config[hp_name] for hp_name in config if config[hp_name] is not None} - - def test_acts_as_mapping(self): - """ - This tests checks that a Configuration can be used as a a dictionary by - checking indexing[], iteration ..., items, keys. - """ - names = ["parent", "child", "friend"] - values = [1, 2, 3] - values_dict = dict(zip(names, values)) - - config = Configuration(self.cs, values=values_dict) - - # Test indexing - assert config["parent"] == values_dict["parent"] - assert config["child"] == values_dict["child"] - - # Test dict methods - assert set(config.keys()) == set(names) - assert set(config.values()) == set(values) - assert set(config.items()) == set(values_dict.items()) - assert len(config) == 3 - - # Test __iter__ - assert set(iter(config)) == set(names) - - # Test unpacking - d = {**config} - assert d == values_dict - - def test_order_of_hyperparameters_is_same_as_config_space(self): - """ - Test the keys respect the contract that they follow the same order that - is present in the ConfigurationSpace. - """ - # Deliberatily different values - config = Configuration(self.cs, values={"child": 2, "parent": 1, "friend": 3}) - assert config.keys() == self.cs.keys() - - def test_multi_sample_quantized_uihp(self): - # This unit test covers a problem with sampling multiple entries at a time from a - # configuration space with at least one UniformIntegerHyperparameter which is quantized. - cs = ConfigurationSpace() - cs.add_hyperparameter( - UniformIntegerHyperparameter("uihp", lower=1, upper=101, q=2, log=False), - ) - - assert cs.sample_configuration() is not None - assert len(cs.sample_configuration(size=10)) == 10 - - def test_meta_field(self): - cs = ConfigurationSpace() - cs.add_hyperparameter( - UniformIntegerHyperparameter("uihp", lower=1, upper=10, meta={"uihp": True}), - ) - cs.add_hyperparameter( - NormalIntegerHyperparameter("nihp", mu=0, sigma=1, meta={"nihp": True}), - ) - cs.add_hyperparameter( - UniformFloatHyperparameter("ufhp", lower=1, upper=10, meta={"ufhp": True}), - ) - cs.add_hyperparameter( - NormalFloatHyperparameter("nfhp", mu=0, sigma=1, meta={"nfhp": True}), - ) - cs.add_hyperparameter( - CategoricalHyperparameter("chp", choices=["1", "2", "3"], meta={"chp": True}), - ) - cs.add_hyperparameter( - OrdinalHyperparameter("ohp", sequence=["1", "2", "3"], meta={"ohp": True}), - ) - cs.add_hyperparameter(Constant("const", value=1, meta={"const": True})) - parent = ConfigurationSpace() - parent.add_configuration_space("sub", cs, delimiter=":") - assert parent["sub:uihp"].meta == {"uihp": True} - assert parent["sub:nihp"].meta == {"nihp": True} - assert parent["sub:ufhp"].meta == {"ufhp": True} - assert parent["sub:nfhp"].meta == {"nfhp": True} - assert parent["sub:chp"].meta == {"chp": True} - assert parent["sub:ohp"].meta == {"ohp": True} - assert parent["sub:const"].meta == {"const": True} - - def test_repr_roundtrip(self): - cs = ConfigurationSpace() - cs.add_hyperparameter(UniformIntegerHyperparameter("uihp", lower=1, upper=10)) - cs.add_hyperparameter(NormalIntegerHyperparameter("nihp", mu=0, sigma=1)) - cs.add_hyperparameter(UniformFloatHyperparameter("ufhp", lower=1, upper=10)) - cs.add_hyperparameter(NormalFloatHyperparameter("nfhp", mu=0, sigma=1)) - cs.add_hyperparameter(CategoricalHyperparameter("chp", choices=["1", "2", "3"])) - cs.add_hyperparameter(OrdinalHyperparameter("ohp", sequence=["1", "2", "3"])) - cs.add_hyperparameter(Constant("const", value=1)) - default = cs.get_default_configuration() - repr = default.__repr__() - repr = repr.replace("})", "}, configuration_space=cs)") - config = eval(repr) - assert default == config + Configuration( + cs, + values={ + "input1": values[0], + "input2": values[1], + "input3": values[2], + "input4": values[3], + "input5": values[4], + "AND": "True", + }, + ) + + +def test_check_configuration2(): + # Test that hyperparameters which are not active must not be set and + # that evaluating forbidden clauses does not choke on missing + # hyperparameters + cs = ConfigurationSpace() + classifier = CategoricalHyperparameter("classifier", ["k_nearest_neighbors", "extra_trees"]) + metric = CategoricalHyperparameter("metric", ["minkowski", "other"]) + p = CategoricalHyperparameter("k_nearest_neighbors:p", [1, 2]) + metric_depends_on_classifier = EqualsCondition(metric, classifier, "k_nearest_neighbors") + p_depends_on_metric = EqualsCondition(p, metric, "minkowski") + cs.add_hyperparameter(metric) + cs.add_hyperparameter(p) + cs.add_hyperparameter(classifier) + cs.add_condition(metric_depends_on_classifier) + cs.add_condition(p_depends_on_metric) + + forbidden = ForbiddenEqualsClause(metric, "other") + cs.add_forbidden_clause(forbidden) + + configuration = Configuration(cs, {"classifier": "extra_trees"}) + + # check backward compatibility with checking configurations instead of vectors + cs.check_configuration(configuration) + + +def test_check_forbidden_with_sampled_vector_configuration(): + cs = ConfigurationSpace() + metric = CategoricalHyperparameter("metric", ["minkowski", "other"]) + cs.add_hyperparameter(metric) + + forbidden = ForbiddenEqualsClause(metric, "other") + cs.add_forbidden_clause(forbidden) + configuration = Configuration(cs, vector=np.ones(1, dtype=float)) + + with pytest.raises(ValueError, match="violates forbidden clause"): + cs._check_forbidden(configuration.get_array()) + + +def test_eq(): + # Compare empty configuration spaces + cs1 = ConfigurationSpace() + cs2 = ConfigurationSpace() + assert cs1 == cs2 + + # Compare to something which isn't a configuration space + assert cs1 != "ConfigurationSpace" + + # Compare to equal configuration spaces + hp1 = CategoricalHyperparameter("parent", [0, 1]) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + hp3 = UniformIntegerHyperparameter("friend", 0, 5) + cond1 = EqualsCondition(hp2, hp1, 0) + cs1.add_hyperparameter(hp1) + cs1.add_hyperparameter(hp2) + cs1.add_condition(cond1) + cs2.add_hyperparameter(hp1) + cs2.add_hyperparameter(hp2) + cs2.add_condition(cond1) + assert cs1 == cs2 + cs1.add_hyperparameter(hp3) + assert cs1 != cs2 + + +def test_neq(): + cs1 = ConfigurationSpace() + assert cs1 != "ConfigurationSpace" + + +def test_repr(): + cs1 = ConfigurationSpace() + retval = cs1.__str__() + assert retval == "Configuration space object:\n Hyperparameters:\n" + + hp1 = CategoricalHyperparameter("parent", [0, 1]) + cs1.add_hyperparameter(hp1) + retval = cs1.__str__() + assert "Configuration space object:\n Hyperparameters:\n %s\n" % str(hp1) == retval + + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cond1 = EqualsCondition(hp2, hp1, 0) + cs1.add_hyperparameter(hp2) + cs1.add_condition(cond1) + retval = cs1.__str__() + assert ( + f"Configuration space object:\n Hyperparameters:\n {hp2!s}\n {hp1!s}\n Conditions:\n {cond2!s}\n" + == retval + ) + + +def test_sample_configuration(): + cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter("parent", [0, 1]) + cs.add_hyperparameter(hp1) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + cs.add_hyperparameter(hp2) + cond1 = EqualsCondition(hp2, hp1, 0) + cs.add_condition(cond1) + # This automatically checks the configuration! + Configuration(cs, {"parent": 0, "child": 5}) + + # and now for something more complicated + cs = ConfigurationSpace(seed=1) + hp1 = CategoricalHyperparameter("input1", [0, 1]) + cs.add_hyperparameter(hp1) + hp2 = CategoricalHyperparameter("input2", [0, 1]) + cs.add_hyperparameter(hp2) + hp3 = CategoricalHyperparameter("input3", [0, 1]) + cs.add_hyperparameter(hp3) + hp4 = CategoricalHyperparameter("input4", [0, 1]) + cs.add_hyperparameter(hp4) + hp5 = CategoricalHyperparameter("input5", [0, 1]) + cs.add_hyperparameter(hp5) + hp6 = Constant("AND", "True") + cs.add_hyperparameter(hp6) + + cond1 = EqualsCondition(hp6, hp1, 1) + cond2 = NotEqualsCondition(hp6, hp2, 1) + cond3 = InCondition(hp6, hp3, [1]) + cond4 = EqualsCondition(hp5, hp3, 1) + cond5 = EqualsCondition(hp4, hp5, 1) + cond6 = EqualsCondition(hp6, hp4, 1) + cond7 = EqualsCondition(hp6, hp5, 1) + + conj1 = AndConjunction(cond1, cond2) + conj2 = OrConjunction(conj1, cond3) + conj3 = AndConjunction(conj2, cond6, cond7) + cs.add_condition(cond4) + cs.add_condition(cond5) + cs.add_condition(conj3) + + samples: list[list[Configuration]] = [] + for i in range(5): + cs.seed(1) + samples.append([]) + for _ in range(100): + sample = cs.sample_configuration() + samples[-1].append(sample) + + if i > 0: + for j in range(100): + assert samples[-1][j] == samples[-2][j] + + +def test_sample_configuration_with_or_conjunction(): + cs = ConfigurationSpace(seed=1) + + hyper_params = {} + hyper_params["hp5"] = CategoricalHyperparameter("hp5", ["0", "1", "2"]) + hyper_params["hp7"] = CategoricalHyperparameter("hp7", ["3", "4", "5"]) + hyper_params["hp8"] = CategoricalHyperparameter("hp8", ["6", "7", "8"]) + for key in hyper_params: + cs.add_hyperparameter(hyper_params[key]) + + cs.add_condition(InCondition(hyper_params["hp5"], hyper_params["hp8"], ["6"])) + + cs.add_condition( + OrConjunction( + InCondition(hyper_params["hp7"], hyper_params["hp8"], ["7"]), + InCondition(hyper_params["hp7"], hyper_params["hp5"], ["1"]), + ), + ) + + for cfg, fixture in zip( + cs.sample_configuration(10), + [ + [1, np.NaN, 2], + [2, np.NaN, np.NaN], + [0, 0, np.NaN], + [0, 2, np.NaN], + [0, 0, np.NaN], + ], + ): + np.testing.assert_array_almost_equal(cfg.get_array(), fixture) + + +def test_sample_wrong_argument(): + cs = ConfigurationSpace() + with pytest.raises(TypeError): + cs.sample_configuration(1.2) # type: ignore + + +def test_sample_no_configuration(): + cs = ConfigurationSpace() + rval = cs.sample_configuration(size=0) + assert len(rval) == 0 + + +def test_subspace_switches(): + # create a switch to select one of two algorithms + algo_switch = CategoricalHyperparameter( + name="switch", + choices=["algo1", "algo2"], + weights=[0.25, 0.75], + default_value="algo1", + ) + + # create sub-configuration space for algorithm 1 + algo1_cs = ConfigurationSpace() + hp1 = CategoricalHyperparameter( + name="algo1_param1", + choices=["A", "B"], + weights=[0.3, 0.7], + default_value="B", + ) + algo1_cs.add_hyperparameter(hp1) + + # create sub-configuration space for algorithm 2 + algo2_cs = ConfigurationSpace() + hp2 = CategoricalHyperparameter(name="algo2_param1", choices=["X", "Y"], default_value="Y") + algo2_cs.add_hyperparameter(hp2) + + # create a configuration space and populate it with both the switch + # and the two sub-configuration spaces + cs = ConfigurationSpace() + cs.add_hyperparameter(algo_switch) + cs.add_configuration_space( + prefix="algo1_subspace", + configuration_space=algo1_cs, + parent_hyperparameter={"parent": algo_switch, "value": "algo1"}, + ) + cs.add_configuration_space( + prefix="algo2_subspace", + configuration_space=algo2_cs, + parent_hyperparameter={"parent": algo_switch, "value": "algo2"}, + ) + + # check choices in the final configuration space + assert cs["switch"].choices == ("algo1", "algo2") + assert cs["algo1_subspace:algo1_param1"].choices == ("A", "B") + assert cs["algo2_subspace:algo2_param1"].choices == ("X", "Y") + + # check probabilities in the final configuration space + assert cs["switch"].probabilities == (0.25, 0.75) + assert cs["algo1_subspace:algo1_param1"].probabilities == (0.3, 0.7) + assert cs["algo2_subspace:algo2_param1"].probabilities == (0.5, 0.5) + + # check default values in the final configuration space + assert cs["switch"].default_value == "algo1" + assert cs["algo1_subspace:algo1_param1"].default_value == "B" + assert cs["algo2_subspace:algo2_param1"].default_value == "Y" + + +def test_acts_as_mapping_2(): + """ + Test that ConfigurationSpace can act as a mapping with iteration, + indexing and items, values, keys. + """ + cs = ConfigurationSpace() + names = [f"name{i}" for i in range(5)] + hyperparameters = [UniformIntegerHyperparameter(name, 0, 10) for name in names] + cs.add_hyperparameters(hyperparameters) + + # Test indexing + assert cs["name3"] == hyperparameters[3] + + # Test dict methods + assert list(cs.keys()) == names + assert list(cs.values()) == hyperparameters + assert list(cs.items()) == list(zip(names, hyperparameters)) + assert len(cs) == 5 + + # Test __iter__ + assert list(iter(cs)) == names + + # Test unpacking + d = {**cs} + assert list(d.keys()) == names + assert list(d.values()) == hyperparameters + assert list(d.items()) == list(zip(names, hyperparameters)) + assert len(d) == 5 + + +def test_remove_hyperparameter_priors(): + cs = ConfigurationSpace() + integer = UniformIntegerHyperparameter("integer", 1, 5, log=True) + cat = CategoricalHyperparameter("cat", [0, 1, 2], weights=[1, 2, 3]) + beta = BetaFloatHyperparameter("beta", alpha=8, beta=2, lower=-1, upper=11) + norm = NormalIntegerHyperparameter("norm", mu=5, sigma=4, lower=1, upper=15) + cs.add_hyperparameters([integer, cat, beta, norm]) + cat_default = cat.default_value + norm_default = norm.default_value + beta_default = beta.default_value + + # add some conditions, to test that remove_parameter_priors keeps the forbiddensdef test_remove_hyp + cond_1 = EqualsCondition(norm, cat, 2) + cond_2 = OrConjunction(EqualsCondition(beta, cat, 0), EqualsCondition(beta, cat, 1)) + cond_3 = OrConjunction( + EqualsCondition(norm, integer, 1), + EqualsCondition(norm, integer, 3), + EqualsCondition(norm, integer, 5), + ) + cs.add_conditions([cond_1, cond_2, cond_3]) + + # add some forbidden clauses too, to test that remove_parameter_priors keeps the forbiddens + forbidden_clause_a = ForbiddenEqualsClause(cat, 0) + forbidden_clause_c = ForbiddenEqualsClause(integer, 3) + forbidden_clause_d = ForbiddenAndConjunction(forbidden_clause_a, forbidden_clause_c) + cs.add_forbidden_clauses([forbidden_clause_c, forbidden_clause_d]) + uniform_cs = cs.remove_hyperparameter_priors() + + expected_cs = ConfigurationSpace() + unif_integer = UniformIntegerHyperparameter("integer", 1, 5, log=True) + unif_cat = CategoricalHyperparameter("cat", [0, 1, 2], default_value=cat_default) + + unif_beta = UniformFloatHyperparameter( + "beta", + lower=-1, + upper=11, + default_value=beta_default, + ) + unif_norm = UniformIntegerHyperparameter( + "norm", + lower=1, + upper=15, + default_value=norm_default, + ) + expected_cs.add_hyperparameters([unif_integer, unif_cat, unif_beta, unif_norm]) + + # add some conditions, to test that remove_parameter_priors keeps the forbiddens + cond_1 = EqualsCondition(unif_norm, unif_cat, 2) + cond_2 = OrConjunction( + EqualsCondition(unif_beta, unif_cat, 0), + EqualsCondition(unif_beta, unif_cat, 1), + ) + cond_3 = OrConjunction( + EqualsCondition(unif_norm, unif_integer, 1), + EqualsCondition(unif_norm, unif_integer, 3), + EqualsCondition(unif_norm, unif_integer, 5), + ) + expected_cs.add_conditions([cond_1, cond_2, cond_3]) + + # add some forbidden clauses too, to test that remove_parameter_priors keeps the forbiddens + forbidden_clause_a = ForbiddenEqualsClause(unif_cat, 0) + forbidden_clause_c = ForbiddenEqualsClause(unif_integer, 3) + forbidden_clause_d = ForbiddenAndConjunction(forbidden_clause_a, forbidden_clause_c) + expected_cs.add_forbidden_clauses([forbidden_clause_c, forbidden_clause_d]) + + # __eq__ not implemented, so this is the next best thing + assert repr(uniform_cs) == repr(expected_cs) + + +def test_substitute_hyperparameters_in_conditions(): + cs1 = ConfigurationSpace() + orig_hp1 = CategoricalHyperparameter("input1", [0, 1]) + orig_hp2 = CategoricalHyperparameter("input2", [0, 1]) + orig_hp3 = UniformIntegerHyperparameter("child1", 0, 10) + orig_hp4 = UniformIntegerHyperparameter("child2", 0, 10) + cs1.add_hyperparameters([orig_hp1, orig_hp2, orig_hp3, orig_hp4]) + cond1 = EqualsCondition(orig_hp2, orig_hp3, 0) + cond2 = EqualsCondition(orig_hp1, orig_hp3, 5) + cond3 = EqualsCondition(orig_hp1, orig_hp4, 1) + andCond = AndConjunction(cond2, cond3) + cs1.add_conditions([cond1, andCond]) + + cs2 = ConfigurationSpace() + sub_hp1 = CategoricalHyperparameter("input1", [0, 1, 2]) + sub_hp2 = CategoricalHyperparameter("input2", [0, 1, 3]) + sub_hp3 = NormalIntegerHyperparameter("child1", lower=0, upper=10, mu=5, sigma=2) + sub_hp4 = BetaIntegerHyperparameter("child2", lower=0, upper=10, alpha=3, beta=5) + cs2.add_hyperparameters([sub_hp1, sub_hp2, sub_hp3, sub_hp4]) + new_conditions = cs1.substitute_hyperparameters_in_conditions(cs1.get_conditions(), cs2) + + test_cond1 = EqualsCondition(sub_hp2, sub_hp3, 0) + test_cond2 = EqualsCondition(sub_hp1, sub_hp3, 5) + test_cond3 = EqualsCondition(sub_hp1, sub_hp4, 1) + test_andCond = AndConjunction(test_cond2, test_cond3) + cs2.add_conditions([test_cond1, test_andCond]) + test_conditions = cs2.get_conditions() + + assert new_conditions[0] == test_conditions[0] + assert new_conditions[1] == test_conditions[1] + + +def test_substitute_hyperparameters_in_inconditions(): + cs1 = ConfigurationSpace() + a = UniformIntegerHyperparameter("a", lower=0, upper=10) + b = UniformFloatHyperparameter("b", lower=1.0, upper=8.0, log=False) + cs1.add_hyperparameters([a, b]) + + cond = InCondition(b, a, [1, 2, 3, 4]) + cs1.add_conditions([cond]) + + cs2 = ConfigurationSpace() + sub_a = UniformIntegerHyperparameter("a", lower=0, upper=10) + sub_b = UniformFloatHyperparameter("b", lower=1.0, upper=8.0, log=False) + cs2.add_hyperparameters([sub_a, sub_b]) + new_conditions = cs1.substitute_hyperparameters_in_conditions(cs1.get_conditions(), cs2) + + test_cond = InCondition(b, a, [1, 2, 3, 4]) + cs2.add_conditions([test_cond]) + test_conditions = cs2.get_conditions() + + assert new_conditions[0] == test_conditions[0] + assert new_conditions[0] is not test_conditions[0] + + assert new_conditions[0].get_parents() == test_conditions[0].get_parents() + assert new_conditions[0].get_parents() is not test_conditions[0].get_parents() + + assert new_conditions[0].get_children() == test_conditions[0].get_children() + assert new_conditions[0].get_children() is not test_conditions[0].get_children() + + +def test_substitute_hyperparameters_in_forbiddens(): + cs1 = ConfigurationSpace() + orig_hp1 = CategoricalHyperparameter("input1", [0, 1]) + orig_hp2 = CategoricalHyperparameter("input2", [0, 1]) + orig_hp3 = UniformIntegerHyperparameter("input3", 0, 10) + orig_hp4 = UniformIntegerHyperparameter("input4", 0, 10) + cs1.add_hyperparameters([orig_hp1, orig_hp2, orig_hp3, orig_hp4]) + forb_1 = ForbiddenEqualsClause(orig_hp1, 0) + forb_2 = ForbiddenEqualsClause(orig_hp2, 1) + forb_3 = ForbiddenEqualsClause(orig_hp3, 10) + forb_4 = ForbiddenAndConjunction(forb_1, forb_2) + forb_5 = ForbiddenLessThanRelation(orig_hp1, orig_hp2) + cs1.add_forbidden_clauses([forb_3, forb_4, forb_5]) + + cs2 = ConfigurationSpace() + sub_hp1 = CategoricalHyperparameter("input1", [0, 1, 2]) + sub_hp2 = CategoricalHyperparameter("input2", [0, 1, 3]) + sub_hp3 = NormalIntegerHyperparameter("input3", lower=0, upper=10, mu=5, sigma=2) + sub_hp4 = BetaIntegerHyperparameter("input4", lower=0, upper=10, alpha=3, beta=5) + cs2.add_hyperparameters([sub_hp1, sub_hp2, sub_hp3, sub_hp4]) + new_forbiddens = cs1.substitute_hyperparameters_in_forbiddens(cs1.get_forbiddens(), cs2) + + test_forb_1 = ForbiddenEqualsClause(sub_hp1, 0) + test_forb_2 = ForbiddenEqualsClause(sub_hp2, 1) + test_forb_3 = ForbiddenEqualsClause(sub_hp3, 10) + test_forb_4 = ForbiddenAndConjunction(test_forb_1, test_forb_2) + test_forb_5 = ForbiddenLessThanRelation(sub_hp1, sub_hp2) + cs2.add_forbidden_clauses([test_forb_3, test_forb_4, test_forb_5]) + test_forbiddens = cs2.get_forbiddens() + + assert new_forbiddens[2] == test_forbiddens[2] + assert new_forbiddens[1] == test_forbiddens[1] + assert new_forbiddens[0] == test_forbiddens[0] + + +def test_estimate_size(): + cs = ConfigurationSpace() + assert cs.estimate_size() == 0 + cs.add_hyperparameter(Constant("constant", 0)) + assert cs.estimate_size() == 1 + cs.add_hyperparameter(UniformIntegerHyperparameter("integer", 0, 5)) + assert cs.estimate_size() == 6 + cs.add_hyperparameter(CategoricalHyperparameter("cat", [0, 1, 2])) + assert cs.estimate_size() == 18 + cs.add_hyperparameter(UniformFloatHyperparameter("float", 0, 1)) + assert np.isinf(cs.estimate_size()) + + +@pytest.fixture +def simple_cs(): + return ConfigurationSpace({"parent": [0, 1], "child": (0, 10), "friend": (0, 5)}) + + +def test_wrong_init(simple_cs: ConfigurationSpace): + with pytest.raises(ValueError): + Configuration(simple_cs) + + with pytest.raises(ValueError): + Configuration(simple_cs, values={}, vector=np.zeros((3,))) + + +def test_init_with_values(simple_cs: ConfigurationSpace): + c1 = Configuration(simple_cs, values={"parent": 1, "child": 2, "friend": 3}) + # Pay attention that the vector does not necessarily has an intuitive + # sorting! + # Values are a little bit higher than one would expect because, + # an integer range of [0,10] is transformed to [-0.499,10.499]. + vector_values = { + "parent": 1, + "child": 0.22727223140405708, + "friend": 0.583333611112037, + } + vector = [0.0] * 3 + for name in simple_cs._hyperparameter_idx: + vector[simple_cs._hyperparameter_idx[name]] = vector_values[name] + c2 = Configuration(simple_cs, vector=vector) + # This tests + # a) that the vector representation of both are the same + # b) that the dictionary representation of both are the same + assert c1 == c2 + + +def test_uniformfloat_transform(): + """This checks whether a value sampled through the configuration + space (it does not happend when the variable is sampled alone) stays + equal when it is serialized via JSON and the deserialized again. + """ + cs = ConfigurationSpace() + a = cs.add_hyperparameter(UniformFloatHyperparameter("a", -5, 10)) + b = cs.add_hyperparameter(NormalFloatHyperparameter("b", 1, 2, log=True)) + for _i in range(100): + config = cs.sample_configuration() + value = OrderedDict(sorted(config.items())) + string = json.dumps(value) + saved_value = json.loads(string) + saved_value = OrderedDict(sorted(byteify(saved_value).items())) + assert repr(value) == repr(saved_value) + + # Next, test whether the truncation also works when initializing the + # Configuration with a dictionary + for _i in range(100): + rs = np.random.RandomState(1) + value_a = a.sample(rs) + value_b = b.sample(rs) + values_dict = {"a": value_a, "b": value_b} + config = Configuration(cs, values=values_dict) + string = json.dumps(dict(config)) + saved_value = json.loads(string) + saved_value = byteify(saved_value) + assert values_dict == saved_value + + +def test_setitem(self): + """Checks overriding a sampled configuration.""" + pcs = ConfigurationSpace() + pcs.add_hyperparameter(UniformIntegerHyperparameter("x0", 1, 5, default_value=1)) + x1 = pcs.add_hyperparameter( + CategoricalHyperparameter("x1", ["ab", "bc", "cd", "de"], default_value="ab"), + ) + + # Condition + x2 = pcs.add_hyperparameter(CategoricalHyperparameter("x2", [1, 2])) + pcs.add_condition(EqualsCondition(x2, x1, "ab")) + + # Forbidden + x3 = pcs.add_hyperparameter(CategoricalHyperparameter("x3", [1, 2])) + pcs.add_forbidden_clause(ForbiddenEqualsClause(x3, 2)) + + conf = pcs.get_default_configuration() + + # failed because it's a invalid configuration + with self.assertRaises(IllegalValueError): + conf["x0"] = 0 + + # failed because the variable didn't exists + with self.assertRaises(HyperparameterNotFoundError): + conf["x_0"] = 1 + + # failed because forbidden clause is violated + with self.assertRaises(ForbiddenValueError): + conf["x3"] = 2 + + assert conf["x3"] == 1 + + # successful operation 1 + x0_old = conf["x0"] + if x0_old == 1: + conf["x0"] = 2 + else: + conf["x0"] = 1 + x0_new = conf["x0"] + assert x0_old != x0_new + pcs._check_configuration_rigorous(conf) + assert conf["x2"] == 1 + + # successful operation 2 + x1_old = conf["x1"] + if x1_old == "ab": + conf["x1"] = "cd" + else: + conf["x1"] = "ab" + x1_new = conf["x1"] + assert x1_old != x1_new + pcs._check_configuration_rigorous(conf) + + with self.assertRaises(KeyError): + conf["x2"] + + +def test_setting_illegal_value(self): + cs = ConfigurationSpace() + cs.add_hyperparameter(UniformFloatHyperparameter("x", 0, 1)) + configuration = {"x": 2} + with self.assertRaises(ValueError): + Configuration(cs, values=configuration) + + +def test_keys(): + # A regression test to make sure issue #49 does no longer pop up. By + # iterating over the configuration in the for loop, it should not raise + # a KeyError if the child hyperparameter is inactive. + cs = ConfigurationSpace() + shrinkage = CategoricalHyperparameter( + "shrinkage", + ["None", "auto", "manual"], + default_value="None", + ) + shrinkage_factor = UniformFloatHyperparameter( + "shrinkage_factor", + 0.0, + 1.0, + 0.5, + ) + cs.add_hyperparameters([shrinkage, shrinkage_factor]) + + cs.add_condition(EqualsCondition(shrinkage_factor, shrinkage, "manual")) + + for _ in range(10): + config = cs.sample_configuration() + {hp_name: config[hp_name] for hp_name in config if config[hp_name] is not None} + + +def test_acts_as_mapping(simple_cs: ConfigurationSpace): + """ + This tests checks that a Configuration can be used as a a dictionary by + checking indexing[], iteration ..., items, keys. + """ + names = ["parent", "child", "friend"] + values = [1, 2, 3] + values_dict = dict(zip(names, values)) + + config = Configuration(simple_cs, values=values_dict) + + # Test indexing + assert config["parent"] == values_dict["parent"] + assert config["child"] == values_dict["child"] + + # Test dict methods + assert set(config.keys()) == set(names) + assert set(config.values()) == set(values) + assert set(config.items()) == set(values_dict.items()) + assert len(config) == 3 + + # Test __iter__ + assert set(iter(config)) == set(names) + + # Test unpacking + d = {**config} + assert d == values_dict + + +def test_order_of_hyperparameters_is_same_as_config_space(simple_cs: ConfigurationSpace): + """ + Test the keys respect the contract that they follow the same order that + is present in the ConfigurationSpace. + """ + # Deliberatily different values + config = Configuration(simple_cs, values={"child": 2, "parent": 1, "friend": 3}) + assert config.keys() == simple_cs.keys() + + +def test_multi_sample_quantized_uihp(): + # This unit test covers a problem with sampling multiple entries at a time from a + # configuration space with at least one UniformIntegerHyperparameter which is quantized. + cs = ConfigurationSpace() + cs.add_hyperparameter( + UniformIntegerHyperparameter("uihp", lower=1, upper=101, q=2, log=False), + ) + + assert cs.sample_configuration() is not None + assert len(cs.sample_configuration(size=10)) == 10 + + +def test_meta_field(): + cs = ConfigurationSpace() + cs.add_hyperparameter( + UniformIntegerHyperparameter("uihp", lower=1, upper=10, meta={"uihp": True}), + ) + cs.add_hyperparameter( + NormalIntegerHyperparameter("nihp", mu=0, sigma=1, meta={"nihp": True}), + ) + cs.add_hyperparameter( + UniformFloatHyperparameter("ufhp", lower=1, upper=10, meta={"ufhp": True}), + ) + cs.add_hyperparameter( + NormalFloatHyperparameter("nfhp", mu=0, sigma=1, meta={"nfhp": True}), + ) + cs.add_hyperparameter( + CategoricalHyperparameter("chp", choices=["1", "2", "3"], meta={"chp": True}), + ) + cs.add_hyperparameter( + OrdinalHyperparameter("ohp", sequence=["1", "2", "3"], meta={"ohp": True}), + ) + cs.add_hyperparameter(Constant("const", value=1, meta={"const": True})) + parent = ConfigurationSpace() + parent.add_configuration_space("sub", cs, delimiter=":") + assert parent["sub:uihp"].meta == {"uihp": True} + assert parent["sub:nihp"].meta == {"nihp": True} + assert parent["sub:ufhp"].meta == {"ufhp": True} + assert parent["sub:nfhp"].meta == {"nfhp": True} + assert parent["sub:chp"].meta == {"chp": True} + assert parent["sub:ohp"].meta == {"ohp": True} + assert parent["sub:const"].meta == {"const": True} + + +def test_repr_roundtrip(): + cs = ConfigurationSpace() + cs.add_hyperparameter(UniformIntegerHyperparameter("uihp", lower=1, upper=10)) + cs.add_hyperparameter(NormalIntegerHyperparameter("nihp", mu=0, sigma=1)) + cs.add_hyperparameter(UniformFloatHyperparameter("ufhp", lower=1, upper=10)) + cs.add_hyperparameter(NormalFloatHyperparameter("nfhp", mu=0, sigma=1)) + cs.add_hyperparameter(CategoricalHyperparameter("chp", choices=["1", "2", "3"])) + cs.add_hyperparameter(OrdinalHyperparameter("ohp", sequence=["1", "2", "3"])) + cs.add_hyperparameter(Constant("const", value=1)) + default = cs.get_default_configuration() + repr = default.__repr__() + repr = repr.replace("})", "}, configuration_space=cs)") + config = eval(repr) + assert default == config diff --git a/test/test_converters_and_test_searchspaces/test_sample_configuration_spaces.py b/test/test_converters_and_test_searchspaces/test_sample_configuration_spaces.py index 91ea4ec7..d7de411f 100644 --- a/test/test_converters_and_test_searchspaces/test_sample_configuration_spaces.py +++ b/test/test_converters_and_test_searchspaces/test_sample_configuration_spaces.py @@ -27,73 +27,51 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import annotations -import os -import unittest +from pathlib import Path + +import pytest import ConfigSpace import ConfigSpace.read_and_write.pcs as pcs_parser import ConfigSpace.read_and_write.pcs_new as pcs_new_parser import ConfigSpace.util +this_file = Path(__file__).absolute().resolve() +this_directory = this_file.parent +configuration_space_path = (this_directory.parent / "test_searchspaces").absolute().resolve() +pcs_files = Path(configuration_space_path).glob("*.pcs") + -class ExampleSearchSpacesTest(unittest.TestCase): - pass +@pytest.mark.parametrize("pcs_file", pcs_files) +def test_autosklearn_space(pcs_file: Path): + try: + with pcs_file.open("r") as fh: + cs = pcs_parser.read(fh) + except Exception: + with pcs_file.open("r") as fh: + cs = pcs_new_parser.read(fh) + default = cs.get_default_configuration() + cs._check_configuration_rigorous(default) + for i in range(10): + neighborhood = ConfigSpace.util.get_one_exchange_neighbourhood(default, seed=i) -def generate(configuration_space_path): - def run_test(self): - try: - with open(configuration_space_path) as fh: - cs = pcs_parser.read(fh) - except Exception: - with open(configuration_space_path) as fh: - cs = pcs_new_parser.read(fh) + for shuffle, n in enumerate(neighborhood): + n.is_valid_configuration() + cs._check_configuration_rigorous(n) + if shuffle == 10: + break - default = cs.get_default_configuration() - cs._check_configuration_rigorous(default) - for i in range(10): - neighborhood = ConfigSpace.util.get_one_exchange_neighbourhood(default, seed=i) + # Sample a little bit + for i in range(10): + cs.seed(i) + for c in cs.sample_configuration(size=5): + c.is_valid_configuration() + cs._check_configuration_rigorous(c) + neighborhood = ConfigSpace.util.get_one_exchange_neighbourhood(c, seed=i) for shuffle, n in enumerate(neighborhood): n.is_valid_configuration() cs._check_configuration_rigorous(n) - if shuffle == 10: + if shuffle == 20: break - - # Sample a little bit - for i in range(10): - cs.seed(i) - configurations = cs.sample_configuration(size=5) - for _j, c in enumerate(configurations): - c.is_valid_configuration() - cs._check_configuration_rigorous(c) - neighborhood = ConfigSpace.util.get_one_exchange_neighbourhood(c, seed=i) - - for shuffle, n in enumerate(neighborhood): - n.is_valid_configuration() - cs._check_configuration_rigorous(n) - if shuffle == 20: - break - - return run_test - - -this_file = os.path.abspath(__file__) -this_directory = os.path.dirname(this_file) -configuration_space_path = os.path.join(this_directory, "..", "test_searchspaces") -configuration_space_path = os.path.abspath(configuration_space_path) -pcs_files = sorted(os.listdir(configuration_space_path)) - -for pcs_file in pcs_files: - if ".pcs" in pcs_file: - full_path = os.path.join(configuration_space_path, pcs_file) - setattr( - ExampleSearchSpacesTest, - "test_%s" % pcs_file.replace(".", "_"), - generate(full_path), - ) - -if __name__ == "__main__": - suite = unittest.TestSuite() - suite.addTest(ExampleSearchSpacesTest(methodName="test_auto-sklearn_2017_04_pcs")) - runner = unittest.TextTestRunner().run(suite) diff --git a/test/test_forbidden.py b/test/test_forbidden.py index e6338213..ce327910 100644 --- a/test/test_forbidden.py +++ b/test/test_forbidden.py @@ -27,10 +27,10 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import annotations -import unittest from itertools import product import numpy as np +import pytest from ConfigSpace import OrdinalHyperparameter @@ -50,267 +50,240 @@ ) -class TestForbidden(unittest.TestCase): - # TODO: return only copies of the objects! - - def test_forbidden_equals_clause(self): - hp1 = CategoricalHyperparameter("parent", [0, 1]) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - hp3 = CategoricalHyperparameter("grandchild", ["hot", "cold"]) - - self.assertRaisesRegex( - ValueError, - r"Forbidden clause must be instantiated with a legal hyperparameter value for " - r"'parent, Type: Categorical, Choices: \{0, 1\}, Default: 0', but got '2'", - ForbiddenEqualsClause, - hp1, - 2, - ) - - forb1 = ForbiddenEqualsClause(hp1, 1) - forb1_ = ForbiddenEqualsClause(hp1, 1) - forb1__ = ForbiddenEqualsClause(hp1, 0) - forb2 = ForbiddenEqualsClause(hp2, 10) - forb3 = ForbiddenEqualsClause(hp3, "hot") - forb3_ = ForbiddenEqualsClause(hp3, "hot") - - assert forb3 == forb3_ - assert forb1 == forb1_ - assert forb1 != "forb1" - assert forb1 != forb2 - assert forb1__ != forb1 - assert str(forb1) == "Forbidden: parent == 1" - - self.assertRaisesRegex( - ValueError, - "Is_forbidden must be called with the " - "instantiated hyperparameter in the " - "forbidden clause; you are missing " - "'parent'", - forb1.is_forbidden, - {1: hp2}, - True, - ) - assert not forb1.is_forbidden({"child": 1}, strict=False) - assert not forb1.is_forbidden({"parent": 0}, True) - assert forb1.is_forbidden({"parent": 1}, True) - - assert forb3.is_forbidden({"grandchild": "hot"}, True) - assert not forb3.is_forbidden({"grandchild": "cold"}, True) - - # Test forbidden on vector values - hyperparameter_idx = {hp1.name: 0, hp2.name: 1} - forb1.set_vector_idx(hyperparameter_idx) - assert not forb1.is_forbidden_vector(np.array([np.NaN, np.NaN]), strict=False) - assert not forb1.is_forbidden_vector(np.array([0.0, np.NaN]), strict=False) - assert forb1.is_forbidden_vector(np.array([1.0, np.NaN]), strict=False) - - def test_in_condition(self): - hp1 = CategoricalHyperparameter("parent", [0, 1, 2, 3, 4]) - hp2 = UniformIntegerHyperparameter("child", 0, 10) - hp3 = UniformIntegerHyperparameter("child2", 0, 10) - hp4 = CategoricalHyperparameter("grandchild", ["hot", "cold", "warm"]) - - self.assertRaisesRegex( - ValueError, - "Forbidden clause must be instantiated with a " - "legal hyperparameter value for " - "'parent, Type: Categorical, Choices: {0, 1, 2, 3, 4}, " - "Default: 0', but got '5'", - ForbiddenInClause, - hp1, - [5], - ) - - forb1 = ForbiddenInClause(hp2, [5, 6, 7, 8, 9]) - forb1_ = ForbiddenInClause(hp2, [9, 8, 7, 6, 5]) - forb2 = ForbiddenInClause(hp2, [5, 6, 7, 8]) - forb3 = ForbiddenInClause(hp3, [5, 6, 7, 8, 9]) - forb4 = ForbiddenInClause(hp4, ["hot", "cold"]) - forb4_ = ForbiddenInClause(hp4, ["hot", "cold"]) - forb5 = ForbiddenInClause(hp1, [3, 4]) - forb5_ = ForbiddenInClause(hp1, [3, 4]) - - assert forb5 == forb5_ - assert forb4 == forb4_ - - assert forb1 == forb1_ - assert forb1 != forb2 - assert forb1 != forb3 - assert str(forb1) == "Forbidden: child in {5, 6, 7, 8, 9}" - self.assertRaisesRegex( - ValueError, - "Is_forbidden must be called with the " - "instantiated hyperparameter in the " - "forbidden clause; you are missing " - "'child'", - forb1.is_forbidden, - {"parent": 1}, +def test_forbidden_equals_clause(): + hp1 = CategoricalHyperparameter("parent", [0, 1]) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + hp3 = CategoricalHyperparameter("grandchild", ["hot", "cold"]) + + with pytest.raises(ValueError): + ForbiddenEqualsClause(hp1, 2) + + forb1 = ForbiddenEqualsClause(hp1, 1) + forb1_ = ForbiddenEqualsClause(hp1, 1) + forb1__ = ForbiddenEqualsClause(hp1, 0) + forb2 = ForbiddenEqualsClause(hp2, 10) + forb3 = ForbiddenEqualsClause(hp3, "hot") + forb3_ = ForbiddenEqualsClause(hp3, "hot") + + assert forb3 == forb3_ + assert forb1 == forb1_ + assert forb1 != "forb1" + assert forb1 != forb2 + assert forb1__ != forb1 + assert str(forb1) == "Forbidden: parent == 1" + + with pytest.raises(ValueError): + forb1.is_forbidden({"child": 1}, True) + assert not forb1.is_forbidden({"child": 1}, strict=False) + assert not forb1.is_forbidden({"parent": 0}, True) + assert forb1.is_forbidden({"parent": 1}, True) + + assert forb3.is_forbidden({"grandchild": "hot"}, True) + assert not forb3.is_forbidden({"grandchild": "cold"}, True) + + # Test forbidden on vector values + hyperparameter_idx = {hp1.name: 0, hp2.name: 1} + forb1.set_vector_idx(hyperparameter_idx) + assert not forb1.is_forbidden_vector(np.array([np.NaN, np.NaN]), strict=False) + assert not forb1.is_forbidden_vector(np.array([0.0, np.NaN]), strict=False) + assert forb1.is_forbidden_vector(np.array([1.0, np.NaN]), strict=False) + + +def test_in_condition(): + hp1 = CategoricalHyperparameter("parent", [0, 1, 2, 3, 4]) + hp2 = UniformIntegerHyperparameter("child", 0, 10) + hp3 = UniformIntegerHyperparameter("child2", 0, 10) + hp4 = CategoricalHyperparameter("grandchild", ["hot", "cold", "warm"]) + + with pytest.raises(ValueError): + ForbiddenInClause(hp1, [5]) + + forb1 = ForbiddenInClause(hp2, [5, 6, 7, 8, 9]) + forb1_ = ForbiddenInClause(hp2, [9, 8, 7, 6, 5]) + forb2 = ForbiddenInClause(hp2, [5, 6, 7, 8]) + forb3 = ForbiddenInClause(hp3, [5, 6, 7, 8, 9]) + forb4 = ForbiddenInClause(hp4, ["hot", "cold"]) + forb4_ = ForbiddenInClause(hp4, ["hot", "cold"]) + forb5 = ForbiddenInClause(hp1, [3, 4]) + forb5_ = ForbiddenInClause(hp1, [3, 4]) + + assert forb5 == forb5_ + assert forb4 == forb4_ + + assert forb1 == forb1_ + assert forb1 != forb2 + assert forb1 != forb3 + assert str(forb1) == "Forbidden: child in {5, 6, 7, 8, 9}" + with pytest.raises(ValueError): + forb1.is_forbidden({"parent": 1}, True) + + assert not forb1.is_forbidden({"parent": 1}, strict=False) + for i in range(5): + assert not forb1.is_forbidden({"child": i}, True) + for i in range(5, 10): + assert forb1.is_forbidden({"child": i}, True) + + assert forb4.is_forbidden({"grandchild": "hot"}, True) + assert forb4.is_forbidden({"grandchild": "cold"}, True) + assert not forb4.is_forbidden({"grandchild": "warm"}, True) + + # Test forbidden on vector values + hyperparameter_idx = {hp1.name: 0, hp2.name: 1} + forb1.set_vector_idx(hyperparameter_idx) + assert not forb1.is_forbidden_vector(np.array([np.NaN, np.NaN]), strict=False) + assert not forb1.is_forbidden_vector(np.array([np.NaN, 0]), strict=False) + correct_vector_value = hp2._inverse_transform(6) + assert forb1.is_forbidden_vector(np.array([np.NaN, correct_vector_value]), strict=False) + + +def test_and_conjunction(): + hp1 = CategoricalHyperparameter("parent", [0, 1]) + hp2 = UniformIntegerHyperparameter("child", 0, 2) + hp3 = UniformIntegerHyperparameter("child2", 0, 2) + hp4 = UniformIntegerHyperparameter("child3", 0, 2) + + forb2 = ForbiddenEqualsClause(hp1, 1) + forb3 = ForbiddenInClause(hp2, range(2, 3)) + forb4 = ForbiddenInClause(hp3, range(2, 3)) + forb5 = ForbiddenInClause(hp4, range(2, 3)) + + and1 = ForbiddenAndConjunction(forb2, forb3) + and2 = ForbiddenAndConjunction(forb2, forb4) + and3 = ForbiddenAndConjunction(forb2, forb5) + + total_and = ForbiddenAndConjunction(and1, and2, and3) + assert ( + str(total_and) + == "((Forbidden: parent == 1 && Forbidden: child in {2}) && (Forbidden: parent == 1 && Forbidden: child2 in {2}) && (Forbidden: parent == 1 && Forbidden: child3 in {2}))" + ) + + results = [ + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + True, + ] + + for i, values in enumerate(product(range(2), range(3), range(3), range(3))): + is_forbidden = total_and.is_forbidden( + {"parent": values[0], "child": values[1], "child2": values[2], "child3": values[3]}, True, ) - assert not forb1.is_forbidden({"parent": 1}, strict=False) - for i in range(0, 5): - assert not forb1.is_forbidden({"child": i}, True) - for i in range(5, 10): - assert forb1.is_forbidden({"child": i}, True) - - assert forb4.is_forbidden({"grandchild": "hot"}, True) - assert forb4.is_forbidden({"grandchild": "cold"}, True) - assert not forb4.is_forbidden({"grandchild": "warm"}, True) - - # Test forbidden on vector values - hyperparameter_idx = {hp1.name: 0, hp2.name: 1} - forb1.set_vector_idx(hyperparameter_idx) - assert not forb1.is_forbidden_vector(np.array([np.NaN, np.NaN]), strict=False) - assert not forb1.is_forbidden_vector(np.array([np.NaN, 0]), strict=False) - correct_vector_value = hp2._inverse_transform(6) - assert forb1.is_forbidden_vector(np.array([np.NaN, correct_vector_value]), strict=False) - - def test_and_conjunction(self): - hp1 = CategoricalHyperparameter("parent", [0, 1]) - hp2 = UniformIntegerHyperparameter("child", 0, 2) - hp3 = UniformIntegerHyperparameter("child2", 0, 2) - hp4 = UniformIntegerHyperparameter("child3", 0, 2) - - forb2 = ForbiddenEqualsClause(hp1, 1) - forb3 = ForbiddenInClause(hp2, range(2, 3)) - forb4 = ForbiddenInClause(hp3, range(2, 3)) - forb5 = ForbiddenInClause(hp4, range(2, 3)) - - and1 = ForbiddenAndConjunction(forb2, forb3) - and2 = ForbiddenAndConjunction(forb2, forb4) - and3 = ForbiddenAndConjunction(forb2, forb5) - - total_and = ForbiddenAndConjunction(and1, and2, and3) - assert ( - str(total_and) - == "((Forbidden: parent == 1 && Forbidden: child in {2}) && (Forbidden: parent == 1 && Forbidden: child2 in {2}) && (Forbidden: parent == 1 && Forbidden: child3 in {2}))" - ) - results = [ - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - False, - True, - ] - - for i, values in enumerate(product(range(2), range(3), range(3), range(3))): - is_forbidden = total_and.is_forbidden( - {"parent": values[0], "child": values[1], "child2": values[2], "child3": values[3]}, - True, - ) - - assert results[i] == is_forbidden - - assert not total_and.is_forbidden({}, strict=False) - - def test_relation(self): - hp1 = CategoricalHyperparameter("cat_int", [0, 1]) - hp2 = OrdinalHyperparameter("ord_int", [0, 1]) - hp3 = UniformIntegerHyperparameter("int", 0, 2) - hp4 = UniformIntegerHyperparameter("int2", 0, 2) - hp5 = UniformFloatHyperparameter("float", 0, 2) - hp6 = CategoricalHyperparameter("str", ["a", "b"]) - hp7 = CategoricalHyperparameter("str2", ["b", "c"]) - - forb = ForbiddenEqualsRelation(hp1, hp2) - assert forb.is_forbidden({"cat_int": 1, "ord_int": 1}, True) - assert not forb.is_forbidden({"cat_int": 0, "ord_int": 1}, True) - - forb = ForbiddenEqualsRelation(hp1, hp3) - assert forb.is_forbidden({"cat_int": 1, "int": 1}, True) - assert not forb.is_forbidden({"cat_int": 0, "int": 1}, True) - - forb = ForbiddenEqualsRelation(hp3, hp4) - assert forb.is_forbidden({"int": 1, "int2": 1}, True) - assert not forb.is_forbidden({"int": 1, "int2": 0}, True) - - forb = ForbiddenLessThanRelation(hp3, hp4) - assert forb.is_forbidden({"int": 0, "int2": 1}, True) - assert not forb.is_forbidden({"int": 1, "int2": 1}, True) - assert not forb.is_forbidden({"int": 1, "int2": 0}, True) - - forb = ForbiddenGreaterThanRelation(hp3, hp4) - assert forb.is_forbidden({"int": 1, "int2": 0}, True) - assert not forb.is_forbidden({"int": 1, "int2": 1}, True) - assert not forb.is_forbidden({"int": 0, "int2": 1}, True) - - forb = ForbiddenGreaterThanRelation(hp4, hp5) - assert forb.is_forbidden({"int2": 1, "float": 0}, True) - assert not forb.is_forbidden({"int2": 1, "float": 1}, True) - assert not forb.is_forbidden({"int2": 0, "float": 1}, True) - - forb = ForbiddenGreaterThanRelation(hp5, hp6) - self.assertRaises(TypeError, forb.is_forbidden, {"float": 1, "str": "b"}, True) - - forb = ForbiddenGreaterThanRelation(hp5, hp7) - self.assertRaises(TypeError, forb.is_forbidden, {"float": 1, "str2": "b"}, True) - - forb = ForbiddenGreaterThanRelation(hp6, hp7) - assert forb.is_forbidden({"str": "b", "str2": "a"}, True) - assert forb.is_forbidden({"str": "c", "str2": "a"}, True) - - forb1 = ForbiddenEqualsRelation(hp2, hp3) - forb2 = ForbiddenEqualsRelation(hp2, hp3) - forb3 = ForbiddenEqualsRelation(hp3, hp4) - assert forb1 == forb2 - assert forb2 != forb3 - - hp1 = OrdinalHyperparameter("water_temperature", ["cold", "luke-warm", "hot", "boiling"]) - hp2 = OrdinalHyperparameter("water_temperature2", ["cold", "luke-warm", "hot", "boiling"]) - forb = ForbiddenGreaterThanRelation(hp1, hp2) - assert not forb.is_forbidden( - {"water_temperature": "boiling", "water_temperature2": "cold"}, - True, - ) - assert forb.is_forbidden({"water_temperature": "hot", "water_temperature2": "cold"}, True) + assert results[i] == is_forbidden + + assert not total_and.is_forbidden({}, strict=False) + + +def test_relation(): + hp1 = CategoricalHyperparameter("cat_int", [0, 1]) + hp2 = OrdinalHyperparameter("ord_int", [0, 1]) + hp3 = UniformIntegerHyperparameter("int", 0, 2) + hp4 = UniformIntegerHyperparameter("int2", 0, 2) + hp5 = UniformFloatHyperparameter("float", 0, 2) + hp6 = CategoricalHyperparameter("str", ["a", "b"]) + hp7 = CategoricalHyperparameter("str2", ["b", "c"]) + + forb = ForbiddenEqualsRelation(hp1, hp2) + assert forb.is_forbidden({"cat_int": 1, "ord_int": 1}, True) + assert not forb.is_forbidden({"cat_int": 0, "ord_int": 1}, True) + + forb = ForbiddenEqualsRelation(hp1, hp3) + assert forb.is_forbidden({"cat_int": 1, "int": 1}, True) + assert not forb.is_forbidden({"cat_int": 0, "int": 1}, True) + + forb = ForbiddenEqualsRelation(hp3, hp4) + assert forb.is_forbidden({"int": 1, "int2": 1}, True) + assert not forb.is_forbidden({"int": 1, "int2": 0}, True) + + forb = ForbiddenLessThanRelation(hp3, hp4) + assert forb.is_forbidden({"int": 0, "int2": 1}, True) + assert not forb.is_forbidden({"int": 1, "int2": 1}, True) + assert not forb.is_forbidden({"int": 1, "int2": 0}, True) + + forb = ForbiddenGreaterThanRelation(hp3, hp4) + assert forb.is_forbidden({"int": 1, "int2": 0}, True) + assert not forb.is_forbidden({"int": 1, "int2": 1}, True) + assert not forb.is_forbidden({"int": 0, "int2": 1}, True) + + forb = ForbiddenGreaterThanRelation(hp4, hp5) + assert forb.is_forbidden({"int2": 1, "float": 0}, True) + assert not forb.is_forbidden({"int2": 1, "float": 1}, True) + assert not forb.is_forbidden({"int2": 0, "float": 1}, True) + + forb = ForbiddenGreaterThanRelation(hp5, hp6) + with pytest.raises(TypeError): + forb.is_forbidden({"float": 1, "str": "b"}, True) + + forb = ForbiddenGreaterThanRelation(hp5, hp7) + with pytest.raises(TypeError): + forb.is_forbidden({"float": 1, "str2": "b"}, True) + + forb = ForbiddenGreaterThanRelation(hp6, hp7) + assert forb.is_forbidden({"str": "b", "str2": "a"}, True) + assert forb.is_forbidden({"str": "c", "str2": "a"}, True) + + forb1 = ForbiddenEqualsRelation(hp2, hp3) + forb2 = ForbiddenEqualsRelation(hp2, hp3) + forb3 = ForbiddenEqualsRelation(hp3, hp4) + assert forb1 == forb2 + assert forb2 != forb3 + + hp1 = OrdinalHyperparameter("water_temperature", ["cold", "luke-warm", "hot", "boiling"]) + hp2 = OrdinalHyperparameter("water_temperature2", ["cold", "luke-warm", "hot", "boiling"]) + forb = ForbiddenGreaterThanRelation(hp1, hp2) + assert not forb.is_forbidden( + {"water_temperature": "boiling", "water_temperature2": "cold"}, + True, + ) + assert forb.is_forbidden({"water_temperature": "hot", "water_temperature2": "cold"}, True) diff --git a/test/test_hyperparameters.py b/test/test_hyperparameters.py index 79d456b2..ad4bef06 100644 --- a/test/test_hyperparameters.py +++ b/test/test_hyperparameters.py @@ -28,7 +28,6 @@ from __future__ import annotations import copy -import unittest from collections import defaultdict from typing import Any @@ -48,3063 +47,3112 @@ UniformIntegerHyperparameter, ) - -class TestHyperparameters(unittest.TestCase): - def setUp(self): - self.meta_data = {"additional": "meta-data", "useful": "for integrations", "input_id": 42} - - def test_constant(self): - # Test construction - c1 = Constant("value", 1) - c2 = Constant("value", 1) - c3 = Constant("value", 2) - c4 = Constant("valuee", 1) - c5 = Constant("valueee", 2) - - # Test attributes are accessible - assert c5.name == "valueee" - assert c5.value == 2 - - # Test the representation - assert c1.__repr__() == "value, Type: Constant, Value: 1" - - # Test the equals operator (and the ne operator in the last line) - assert c1 != 1 - assert c1 == c2 - assert c1 != c3 - assert c1 != c4 - assert c1 != c5 - - # Test that only string, integers and floats are allowed - v: Any - for v in [{}, None, True]: - with self.assertRaises(TypeError): - Constant("value", v) - - # Test that only string names are allowed - for name in [1, {}, None, True]: - with self.assertRaises(TypeError): - Constant(name, "value") - - # test that meta-data is stored correctly - c1_meta = Constant("value", 1, dict(self.meta_data)) - assert c1_meta.meta == self.meta_data - - # Test getting the size - for constant in (c1, c2, c3, c4, c5, c1_meta): - assert constant.get_size() == 1 - - def test_constant_pdf(self): - c1 = Constant("valuee", 1) - c2 = Constant("valueee", -2) - - # TODO - change this once the is_legal support is there - should then be zero - # but does not have an actual impact of now - point_1 = np.array([1]) - point_2 = np.array([-2]) - array_1 = np.array([1, 1]) - array_2 = np.array([-2, -2]) - array_3 = np.array([1, -2]) - - wrong_shape_1 = np.array([[1]]) - wrong_shape_2 = np.array([1, 2, 3]).reshape(1, -1) - wrong_shape_3 = np.array([1, 2, 3]).reshape(-1, 1) - - assert c1.pdf(point_1) == np.array([1.0]) - assert c2.pdf(point_2) == np.array([1.0]) - assert c1.pdf(point_2) == np.array([0.0]) - assert c2.pdf(point_1) == np.array([0.0]) - - assert tuple(c1.pdf(array_1)) == tuple(np.array([1.0, 1.0])) - assert tuple(c2.pdf(array_2)) == tuple(np.array([1.0, 1.0])) - assert tuple(c1.pdf(array_2)) == tuple(np.array([0.0, 0.0])) - assert tuple(c1.pdf(array_3)) == tuple(np.array([1.0, 0.0])) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1.pdf(0.2) - with self.assertRaises(TypeError): - c1.pdf("pdf") - - # and it must be one-dimensional - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_1) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_2) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_3) - - def test_constant__pdf(self): - c1 = Constant("valuee", 1) - c2 = Constant("valueee", -2) - - point_1 = np.array([1]) - point_2 = np.array([-2]) - array_1 = np.array([1, 1]) - array_2 = np.array([-2, -2]) - array_3 = np.array([1, -2]) - - # These shapes are allowed in _pdf - accepted_shape_1 = np.array([[1]]) - accepted_shape_2 = np.array([1, 2, 3]).reshape(1, -1) - accepted_shape_3 = np.array([3, 2, 1]).reshape(-1, 1) - - assert c1._pdf(point_1) == np.array([1.0]) - assert c2._pdf(point_2) == np.array([1.0]) - assert c1._pdf(point_2) == np.array([0.0]) - assert c2._pdf(point_1) == np.array([0.0]) - - # Only (N, ) numpy arrays are seamlessly converted to tuples - # so the __eq__ method works as intended - assert tuple(c1._pdf(array_1)) == tuple(np.array([1.0, 1.0])) - assert tuple(c2._pdf(array_2)) == tuple(np.array([1.0, 1.0])) - assert tuple(c1._pdf(array_2)) == tuple(np.array([0.0, 0.0])) - assert tuple(c1._pdf(array_3)) == tuple(np.array([1.0, 0.0])) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1._pdf(0.2) - with self.assertRaises(TypeError): - c1._pdf("pdf") - - # Simply check that it runs, since _pdf does not restrict shape (only public method does) - c1._pdf(accepted_shape_1) - c1._pdf(accepted_shape_2) - c1._pdf(accepted_shape_3) - - def test_constant_get_max_density(self): - c1 = Constant("valuee", 1) - c2 = Constant("valueee", -2) - assert c1.get_max_density() == 1.0 - assert c2.get_max_density() == 1.0 - - def test_uniformfloat(self): - # TODO test non-equality - # TODO test sampling from a log-distribution which has a negative - # lower value! - f1 = UniformFloatHyperparameter("param", 0, 10) - f1_ = UniformFloatHyperparameter("param", 0, 10) - assert f1 == f1_ - assert str(f1) == "param, Type: UniformFloat, Range: [0.0, 10.0], Default: 5.0" - - # Test attributes are accessible - assert f1.name == "param" - self.assertAlmostEqual(f1.lower, 0.0) - self.assertAlmostEqual(f1.upper, 10.0) - assert f1.q is None - assert f1.log is False - self.assertAlmostEqual(f1.default_value, 5.0) - self.assertAlmostEqual(f1.normalized_default_value, 0.5) - - f2 = UniformFloatHyperparameter("param", 0, 10, q=0.1) - f2_ = UniformFloatHyperparameter("param", 0, 10, q=0.1) - assert f2 == f2_ - assert str(f2) == "param, Type: UniformFloat, Range: [0.0, 10.0], Default: 5.0, Q: 0.1" - - f3 = UniformFloatHyperparameter("param", 0.00001, 10, log=True) - f3_ = UniformFloatHyperparameter("param", 0.00001, 10, log=True) - assert f3 == f3_ - assert ( - str(f3) - == "param, Type: UniformFloat, Range: [1e-05, 10.0], Default: 0.01, on log-scale" - ) - - f4 = UniformFloatHyperparameter("param", 0, 10, default_value=1.0) - f4_ = UniformFloatHyperparameter("param", 0, 10, default_value=1.0) - # Test that a int default is converted to float - f4__ = UniformFloatHyperparameter("param", 0, 10, default_value=1) - assert f4 == f4_ - assert isinstance(f4.default_value, type(f4__.default_value)) - assert str(f4) == "param, Type: UniformFloat, Range: [0.0, 10.0], Default: 1.0" - - f5 = UniformFloatHyperparameter("param", 0.1, 10, q=0.1, log=True, default_value=1.0) - f5_ = UniformFloatHyperparameter("param", 0.1, 10, q=0.1, log=True, default_value=1.0) - assert f5 == f5_ - assert ( - str(f5) - == "param, Type: UniformFloat, Range: [0.1, 10.0], Default: 1.0, on log-scale, Q: 0.1" - ) - - assert f1 != f2 - assert f1 != "UniformFloat" - - # test that meta-data is stored correctly - f_meta = UniformFloatHyperparameter( - "param", - 0.1, - 10, - q=0.1, - log=True, - default_value=1.0, - meta=dict(self.meta_data), - ) - assert f_meta.meta == self.meta_data - - # Test get_size - for float_hp in (f1, f3, f4): - assert np.isinf(float_hp.get_size()) - assert f2.get_size() == 101 - assert f5.get_size() == 100 - - def test_uniformfloat_to_integer(self): - f1 = UniformFloatHyperparameter("param", 1, 10, q=0.1, log=True) - with pytest.warns( - UserWarning, - match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", - ): - f2 = f1.to_integer() - # TODO is this a useful rounding? - # TODO should there be any rounding, if e.g. lower=0.1 - assert str(f2) == "param, Type: UniformInteger, Range: [1, 10], Default: 3, on log-scale" - - def test_uniformfloat_is_legal(self): - lower = 0.1 - upper = 10 - f1 = UniformFloatHyperparameter("param", lower, upper, q=0.1, log=True) - - assert f1.is_legal(3.0) - assert f1.is_legal(3) - assert not f1.is_legal(-0.1) - assert not f1.is_legal(10.1) - assert not f1.is_legal("AAA") - assert not f1.is_legal({}) - - # Test legal vector values - assert f1.is_legal_vector(1.0) - assert f1.is_legal_vector(0.0) - assert f1.is_legal_vector(0) - assert f1.is_legal_vector(0.3) - assert not f1.is_legal_vector(-0.1) - assert not f1.is_legal_vector(1.1) - self.assertRaises(TypeError, f1.is_legal_vector, "Hahaha") - - def test_uniformfloat_illegal_bounds(self): - self.assertRaisesRegex( - ValueError, - r"Negative lower bound \(0.000000\) for log-scale hyperparameter " - r"param is forbidden.", - UniformFloatHyperparameter, - "param", - 0, - 10, - q=0.1, - log=True, - ) - - self.assertRaisesRegex( - ValueError, - "Upper bound 0.000000 must be larger than lower bound " - "1.000000 for hyperparameter param", - UniformFloatHyperparameter, - "param", - 1, - 0, - ) - - def test_uniformfloat_pdf(self): - c1 = UniformFloatHyperparameter("param", lower=0, upper=10) - c2 = UniformFloatHyperparameter("logparam", lower=np.exp(0), upper=np.exp(10), log=True) - c3 = UniformFloatHyperparameter("param", lower=0, upper=0.5) - - point_1 = np.array([3]) - point_2 = np.array([7]) - point_3 = np.array([0.3]) - array_1 = np.array([3, 7, 5]) - point_outside_range = np.array([-1]) - point_outside_range_log = np.array([0.1]) - wrong_shape_1 = np.array([[3]]) - wrong_shape_2 = np.array([3, 5, 7]).reshape(1, -1) - wrong_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) - - self.assertAlmostEqual(c1.pdf(point_1)[0], 0.1) - self.assertAlmostEqual(c2.pdf(point_2)[0], 4.539992976248485e-05) - self.assertAlmostEqual(c1.pdf(point_1)[0], 0.1) - self.assertAlmostEqual(c2.pdf(point_2)[0], 4.539992976248485e-05) - self.assertAlmostEqual(c3.pdf(point_3)[0], 2.0) - - # TODO - change this once the is_legal support is there - # but does not have an actual impact of now - # since inverse_transform pulls everything into range, - # even points outside get evaluated in range - self.assertAlmostEqual(c1.pdf(point_outside_range)[0], 0.1) - self.assertAlmostEqual(c2.pdf(point_outside_range_log)[0], 4.539992976248485e-05) - - # this, however, is a negative value on a log param, which cannot be pulled into range - with pytest.warns(RuntimeWarning, match="invalid value encountered in log"): - assert c2.pdf(point_outside_range)[0] == 0.0 - - array_results = c1.pdf(array_1) - array_results_log = c2.pdf(array_1) - expected_results = np.array([0.1, 0.1, 0.1]) - expected_log_results = np.array( - [4.539992976248485e-05, 4.539992976248485e-05, 4.539992976248485e-05], - ) - assert array_results.shape == expected_results.shape - assert array_results_log.shape == expected_log_results.shape - for res, log_res, exp_res, exp_log_res in zip( - array_results, - array_results_log, - expected_results, - expected_log_results, - ): - self.assertAlmostEqual(res, exp_res) - self.assertAlmostEqual(log_res, exp_log_res) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1.pdf(0.2) - with self.assertRaises(TypeError): - c1.pdf("pdf") - - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_1) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_2) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_3) - - def test_uniformfloat__pdf(self): - c1 = UniformFloatHyperparameter("param", lower=0, upper=10) - c2 = UniformFloatHyperparameter("logparam", lower=np.exp(0), upper=np.exp(10), log=True) - c3 = UniformFloatHyperparameter("param", lower=0, upper=0.5) - - point_1 = np.array([0.3]) - point_2 = np.array([1]) - point_3 = np.array([0.0]) - array_1 = np.array([0.3, 0.7, 1.01]) - point_outside_range_1 = np.array([-1]) - point_outside_range_2 = np.array([1.1]) - accepted_shape_1 = np.array([[0.3]]) - accepted_shape_2 = np.array([0.3, 0.5, 1.1]).reshape(1, -1) - accepted_shape_3 = np.array([1.1, 0.5, 0.3]).reshape(-1, 1) - - self.assertAlmostEqual(c1._pdf(point_1)[0], 0.1) - self.assertAlmostEqual(c2._pdf(point_2)[0], 4.539992976248485e-05) - self.assertAlmostEqual(c1._pdf(point_1)[0], 0.1) - self.assertAlmostEqual(c2._pdf(point_2)[0], 4.539992976248485e-05) - self.assertAlmostEqual(c3._pdf(point_3)[0], 2.0) - - # TODO - change this once the is_legal support is there - # but does not have an actual impact of now - # since inverse_transform pulls everything into range, - # even points outside get evaluated in range - self.assertAlmostEqual(c1._pdf(point_outside_range_1)[0], 0.0) - self.assertAlmostEqual(c2._pdf(point_outside_range_2)[0], 0.0) - self.assertAlmostEqual(c1._pdf(point_outside_range_2)[0], 0.0) - self.assertAlmostEqual(c2._pdf(point_outside_range_1)[0], 0.0) - - array_results = c1._pdf(array_1) - array_results_log = c2._pdf(array_1) - expected_results = np.array([0.1, 0.1, 0]) - expected_log_results = np.array([4.539992976248485e-05, 4.539992976248485e-05, 0.0]) - assert array_results.shape == expected_results.shape - assert array_results_log.shape == expected_log_results.shape - for res, log_res, exp_res, exp_log_res in zip( - array_results, - array_results_log, - expected_results, - expected_log_results, - ): - self.assertAlmostEqual(res, exp_res) - self.assertAlmostEqual(log_res, exp_log_res) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1._pdf(0.2) - with self.assertRaises(TypeError): - c1._pdf("pdf") - - # Simply check that it runs, since _pdf does not restrict shape (only public method does) - c1._pdf(accepted_shape_1) - c1._pdf(accepted_shape_2) - c1._pdf(accepted_shape_3) - - def test_uniformfloat_get_max_density(self): - c1 = UniformFloatHyperparameter("param", lower=0, upper=10) - c2 = UniformFloatHyperparameter("logparam", lower=np.exp(0), upper=np.exp(10), log=True) - c3 = UniformFloatHyperparameter("param", lower=0, upper=0.5) - assert c1.get_max_density() == 0.1 - self.assertAlmostEqual(c2.get_max_density(), 4.539992976248485e-05) - assert c3.get_max_density() == 2 - - def test_normalfloat(self): - # TODO test non-equality - f1 = NormalFloatHyperparameter("param", 0.5, 10.5) - f1_ = NormalFloatHyperparameter("param", 0.5, 10.5) - assert f1 == f1_ - assert str(f1) == "param, Type: NormalFloat, Mu: 0.5 Sigma: 10.5, Default: 0.5" - - # Due to seemingly different numbers with x86_64 and i686 architectures - # we got these numbers, where last two are slightly different - # 5.715498606617943, -0.9517751622974389, - # 7.3007296500572725, 16.49181349228427 - # They are equal up to 14 decimal places - expected = [5.715498606617943, -0.9517751622974389, 7.300729650057271, 16.491813492284265] - np.testing.assert_almost_equal( - f1.get_neighbors(0.5, rs=np.random.RandomState(42)), - expected, - decimal=14, - ) - - # Test attributes are accessible - assert f1.name == "param" - self.assertAlmostEqual(f1.mu, 0.5) - self.assertAlmostEqual(f1.sigma, 10.5) - self.assertAlmostEqual(f1.q, None) - assert f1.log is False - self.assertAlmostEqual(f1.default_value, 0.5) - self.assertAlmostEqual(f1.normalized_default_value, 0.5) - - # Test copy - copy_f1 = copy.copy(f1) - - assert copy_f1.name == f1.name - assert copy_f1.mu == f1.mu - assert copy_f1.sigma == f1.sigma - assert copy_f1.default_value == f1.default_value - - f2 = NormalFloatHyperparameter("param", 0, 10, q=0.1) - f2_ = NormalFloatHyperparameter("param", 0, 10, q=0.1) - assert f2 == f2_ - assert str(f2) == "param, Type: NormalFloat, Mu: 0.0 Sigma: 10.0, Default: 0.0, Q: 0.1" - - f3 = NormalFloatHyperparameter("param", 0, 10, log=True) - f3_ = NormalFloatHyperparameter("param", 0, 10, log=True) - assert f3 == f3_ - assert ( - str(f3) == "param, Type: NormalFloat, Mu: 0.0 Sigma: 10.0, Default: 1.0, on log-scale" - ) - - f4 = NormalFloatHyperparameter("param", 0, 10, default_value=1.0) - f4_ = NormalFloatHyperparameter("param", 0, 10, default_value=1.0) - assert f4 == f4_ - assert str(f4) == "param, Type: NormalFloat, Mu: 0.0 Sigma: 10.0, Default: 1.0" - - f5 = NormalFloatHyperparameter("param", 0, 10, default_value=3.0, q=0.1, log=True) - f5_ = NormalFloatHyperparameter("param", 0, 10, default_value=3.0, q=0.1, log=True) - assert f5 == f5_ - assert ( - str(f5) - == "param, Type: NormalFloat, Mu: 0.0 Sigma: 10.0, Default: 3.0, on log-scale, Q: 0.1" - ) - - assert f1 != f2 - assert f1 != "UniformFloat" - - with pytest.raises(ValueError): - f6 = NormalFloatHyperparameter( - "param", - 5, - 10, - lower=0.1, - upper=0.1, - default_value=5.0, - q=0.1, - log=True, - ) - - with pytest.raises(ValueError): - f6 = NormalFloatHyperparameter( - "param", - 5, - 10, - lower=0.1, - default_value=5.0, - q=0.1, - log=True, - ) - - with pytest.raises(ValueError): - f6 = NormalFloatHyperparameter( - "param", - 5, - 10, - upper=0.1, - default_value=5.0, - q=0.1, - log=True, - ) - +META_DATA = {"additional": "meta-data", "useful": "for integrations", "input_id": 42} + + +def test_constant(): + # Test construction + c1 = Constant("value", 1) + c2 = Constant("value", 1) + c3 = Constant("value", 2) + c4 = Constant("valuee", 1) + c5 = Constant("valueee", 2) + + # Test attributes are accessible + assert c5.name == "valueee" + assert c5.value == 2 + + # Test the representation + assert c1.__repr__() == "value, Type: Constant, Value: 1" + + # Test the equals operator (and the ne operator in the last line) + assert c1 != 1 + assert c1 == c2 + assert c1 != c3 + assert c1 != c4 + assert c1 != c5 + + # Test that only string, integers and floats are allowed + v: Any + for v in [{}, None, True]: + with pytest.raises(TypeError): + Constant("value", v) + + # Test that only string names are allowed + for name in [1, {}, None, True]: + with pytest.raises(TypeError): + Constant(name, "value") + + # test that meta-data is stored correctly + c1_meta = Constant("value", 1, dict(META_DATA)) + assert c1_meta.meta == META_DATA + + # Test getting the size + for constant in (c1, c2, c3, c4, c5, c1_meta): + assert constant.get_size() == 1 + + +def test_constant_pdf(): + c1 = Constant("valuee", 1) + c2 = Constant("valueee", -2) + + # TODO - change this once the is_legal support is there - should then be zero + # but does not have an actual impact of now + point_1 = np.array([1]) + point_2 = np.array([-2]) + array_1 = np.array([1, 1]) + array_2 = np.array([-2, -2]) + array_3 = np.array([1, -2]) + + wrong_shape_1 = np.array([[1]]) + wrong_shape_2 = np.array([1, 2, 3]).reshape(1, -1) + wrong_shape_3 = np.array([1, 2, 3]).reshape(-1, 1) + + assert c1.pdf(point_1) == np.array([1.0]) + assert c2.pdf(point_2) == np.array([1.0]) + assert c1.pdf(point_2) == np.array([0.0]) + assert c2.pdf(point_1) == np.array([0.0]) + + assert tuple(c1.pdf(array_1)) == tuple(np.array([1.0, 1.0])) + assert tuple(c2.pdf(array_2)) == tuple(np.array([1.0, 1.0])) + assert tuple(c1.pdf(array_2)) == tuple(np.array([0.0, 0.0])) + assert tuple(c1.pdf(array_3)) == tuple(np.array([1.0, 0.0])) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1.pdf(0.2) + with pytest.raises(TypeError): + c1.pdf("pdf") + + # and it must be one-dimensional + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_1) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_2) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_3) + + +def test_constant__pdf(): + c1 = Constant("valuee", 1) + c2 = Constant("valueee", -2) + + point_1 = np.array([1]) + point_2 = np.array([-2]) + array_1 = np.array([1, 1]) + array_2 = np.array([-2, -2]) + array_3 = np.array([1, -2]) + + # These shapes are allowed in _pdf + accepted_shape_1 = np.array([[1]]) + accepted_shape_2 = np.array([1, 2, 3]).reshape(1, -1) + accepted_shape_3 = np.array([3, 2, 1]).reshape(-1, 1) + + assert c1._pdf(point_1) == np.array([1.0]) + assert c2._pdf(point_2) == np.array([1.0]) + assert c1._pdf(point_2) == np.array([0.0]) + assert c2._pdf(point_1) == np.array([0.0]) + + # Only (N, ) numpy arrays are seamlessly converted to tuples + # so the __eq__ method works as intended + assert tuple(c1._pdf(array_1)) == tuple(np.array([1.0, 1.0])) + assert tuple(c2._pdf(array_2)) == tuple(np.array([1.0, 1.0])) + assert tuple(c1._pdf(array_2)) == tuple(np.array([0.0, 0.0])) + assert tuple(c1._pdf(array_3)) == tuple(np.array([1.0, 0.0])) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1._pdf(0.2) + with pytest.raises(TypeError): + c1._pdf("pdf") + + # Simply check that it runs, since _pdf does not restrict shape (only public method does) + c1._pdf(accepted_shape_1) + c1._pdf(accepted_shape_2) + c1._pdf(accepted_shape_3) + + +def test_constant_get_max_density(): + c1 = Constant("valuee", 1) + c2 = Constant("valueee", -2) + assert c1.get_max_density() == 1.0 + assert c2.get_max_density() == 1.0 + + +def test_uniformfloat(): + # TODO test non-equality + # TODO test sampling from a log-distribution which has a negative + # lower value! + f1 = UniformFloatHyperparameter("param", 0, 10) + f1_ = UniformFloatHyperparameter("param", 0, 10) + assert f1 == f1_ + assert str(f1) == "param, Type: UniformFloat, Range: [0.0, 10.0], Default: 5.0" + + # Test attributes are accessible + assert f1.name == "param" + assert f1.lower == pytest.approx(0.0) + assert f1.upper == pytest.approx(10.0) + assert f1.q is None + assert f1.log is False + assert f1.default_value == pytest.approx(5.0) + assert f1.normalized_default_value == pytest.approx(0.5) + + f2 = UniformFloatHyperparameter("param", 0, 10, q=0.1) + f2_ = UniformFloatHyperparameter("param", 0, 10, q=0.1) + assert f2 == f2_ + assert str(f2) == "param, Type: UniformFloat, Range: [0.0, 10.0], Default: 5.0, Q: 0.1" + + f3 = UniformFloatHyperparameter("param", 0.00001, 10, log=True) + f3_ = UniformFloatHyperparameter("param", 0.00001, 10, log=True) + assert f3 == f3_ + assert str(f3) == "param, Type: UniformFloat, Range: [1e-05, 10.0], Default: 0.01, on log-scale" + + f4 = UniformFloatHyperparameter("param", 0, 10, default_value=1.0) + f4_ = UniformFloatHyperparameter("param", 0, 10, default_value=1.0) + # Test that a int default is converted to float + f4__ = UniformFloatHyperparameter("param", 0, 10, default_value=1) + assert f4 == f4_ + assert isinstance(f4.default_value, type(f4__.default_value)) + assert str(f4) == "param, Type: UniformFloat, Range: [0.0, 10.0], Default: 1.0" + + f5 = UniformFloatHyperparameter("param", 0.1, 10, q=0.1, log=True, default_value=1.0) + f5_ = UniformFloatHyperparameter("param", 0.1, 10, q=0.1, log=True, default_value=1.0) + assert f5 == f5_ + assert ( + str(f5) + == "param, Type: UniformFloat, Range: [0.1, 10.0], Default: 1.0, on log-scale, Q: 0.1" + ) + + assert f1 != f2 + assert f1 != "UniformFloat" + + # test that meta-data is stored correctly + f_meta = UniformFloatHyperparameter( + "param", + 0.1, + 10, + q=0.1, + log=True, + default_value=1.0, + meta=dict(META_DATA), + ) + assert f_meta.meta == META_DATA + + # Test get_size + for float_hp in (f1, f3, f4): + assert np.isinf(float_hp.get_size()) + assert f2.get_size() == 101 + assert f5.get_size() == 100 + + +def test_uniformfloat_to_integer(): + f1 = UniformFloatHyperparameter("param", 1, 10, q=0.1, log=True) + with pytest.warns( + UserWarning, + match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", + ): + f2 = f1.to_integer() + # TODO is this a useful rounding? + # TODO should there be any rounding, if e.g. lower=0.1 + assert str(f2) == "param, Type: UniformInteger, Range: [1, 10], Default: 3, on log-scale" + + +def test_uniformfloat_is_legal(): + lower = 0.1 + upper = 10 + f1 = UniformFloatHyperparameter("param", lower, upper, q=0.1, log=True) + + assert f1.is_legal(3.0) + assert f1.is_legal(3) + assert not f1.is_legal(-0.1) + assert not f1.is_legal(10.1) + assert not f1.is_legal("AAA") + assert not f1.is_legal({}) + + # Test legal vector values + assert f1.is_legal_vector(1.0) + assert f1.is_legal_vector(0.0) + assert f1.is_legal_vector(0) + assert f1.is_legal_vector(0.3) + assert not f1.is_legal_vector(-0.1) + assert not f1.is_legal_vector(1.1) + with pytest.raises(TypeError): + f1.is_legal_vector("Hahaha") + + +def test_uniformfloat_illegal_bounds(): + with pytest.raises( + ValueError, + match=r"Negative lower bound \(0.000000\) for log-scale hyperparameter " r"param is forbidden.", + ): + _ = UniformFloatHyperparameter("param", 0, 10, q=0.1, log=True) + + with pytest.raises( + ValueError, + match="Upper bound 0.000000 must be larger than lower bound " "1.000000 for hyperparameter param", + ): + _ = UniformFloatHyperparameter("param", 1, 0) + + +def test_uniformfloat_pdf(): + c1 = UniformFloatHyperparameter("param", lower=0, upper=10) + c2 = UniformFloatHyperparameter("logparam", lower=np.exp(0), upper=np.exp(10), log=True) + c3 = UniformFloatHyperparameter("param", lower=0, upper=0.5) + + point_1 = np.array([3]) + point_2 = np.array([7]) + point_3 = np.array([0.3]) + array_1 = np.array([3, 7, 5]) + point_outside_range = np.array([-1]) + point_outside_range_log = np.array([0.1]) + wrong_shape_1 = np.array([[3]]) + wrong_shape_2 = np.array([3, 5, 7]).reshape(1, -1) + wrong_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) + + assert c1.pdf(point_1)[0] == pytest.approx(0.1) + assert c2.pdf(point_2)[0] == pytest.approx(4.539992976248485e-05) + assert c1.pdf(point_1)[0] == pytest.approx(0.1) + assert c2.pdf(point_2)[0] == pytest.approx(4.539992976248485e-05) + assert c3.pdf(point_3)[0] == pytest.approx(2.0) + + # TODO - change this once the is_legal support is there + # but does not have an actual impact of now + # since inverse_transform pulls everything into range, + # even points outside get evaluated in range + assert c1.pdf(point_outside_range)[0] == pytest.approx(0.1) + assert c2.pdf(point_outside_range_log)[0] == pytest.approx(4.539992976248485e-05) + + # this, however, is a negative value on a log param, which cannot be pulled into range + with pytest.warns(RuntimeWarning, match="invalid value encountered in log"): + assert c2.pdf(point_outside_range)[0] == 0.0 + + array_results = c1.pdf(array_1) + array_results_log = c2.pdf(array_1) + expected_results = np.array([0.1, 0.1, 0.1]) + expected_log_results = np.array( + [4.539992976248485e-05, 4.539992976248485e-05, 4.539992976248485e-05], + ) + assert array_results.shape == expected_results.shape + assert array_results_log.shape == expected_log_results.shape + for res, log_res, exp_res, exp_log_res in zip( + array_results, + array_results_log, + expected_results, + expected_log_results, + ): + assert res == pytest.approx(exp_res) + assert log_res == pytest.approx(exp_log_res) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1.pdf(0.2) + with pytest.raises(TypeError): + c1.pdf("pdf") + + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_1) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_2) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_3) + + +def test_uniformfloat__pdf(): + c1 = UniformFloatHyperparameter("param", lower=0, upper=10) + c2 = UniformFloatHyperparameter("logparam", lower=np.exp(0), upper=np.exp(10), log=True) + c3 = UniformFloatHyperparameter("param", lower=0, upper=0.5) + + point_1 = np.array([0.3]) + point_2 = np.array([1]) + point_3 = np.array([0.0]) + array_1 = np.array([0.3, 0.7, 1.01]) + point_outside_range_1 = np.array([-1]) + point_outside_range_2 = np.array([1.1]) + accepted_shape_1 = np.array([[0.3]]) + accepted_shape_2 = np.array([0.3, 0.5, 1.1]).reshape(1, -1) + accepted_shape_3 = np.array([1.1, 0.5, 0.3]).reshape(-1, 1) + + assert c1._pdf(point_1)[0] == pytest.approx(0.1) + assert c2._pdf(point_2)[0] == pytest.approx(4.539992976248485e-05) + assert c1._pdf(point_1)[0] == pytest.approx(0.1) + assert c2._pdf(point_2)[0] == pytest.approx(4.539992976248485e-05) + assert c3._pdf(point_3)[0] == pytest.approx(2.0) + + # TODO - change this once the is_legal support is there + # but does not have an actual impact of now + # since inverse_transform pulls everything into range, + # even points outside get evaluated in range + assert c1._pdf(point_outside_range_1)[0] == pytest.approx(0.0) + assert c2._pdf(point_outside_range_2)[0] == pytest.approx(0.0) + assert c1._pdf(point_outside_range_2)[0] == pytest.approx(0.0) + assert c2._pdf(point_outside_range_1)[0] == pytest.approx(0.0) + + array_results = c1._pdf(array_1) + array_results_log = c2._pdf(array_1) + expected_results = np.array([0.1, 0.1, 0]) + expected_log_results = np.array([4.539992976248485e-05, 4.539992976248485e-05, 0.0]) + assert array_results.shape == expected_results.shape + assert array_results_log.shape == expected_log_results.shape + for res, log_res, exp_res, exp_log_res in zip( + array_results, + array_results_log, + expected_results, + expected_log_results, + ): + assert res == pytest.approx(exp_res) + assert log_res == pytest.approx(exp_log_res) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1._pdf(0.2) + with pytest.raises(TypeError): + c1._pdf("pdf") + + # Simply check that it runs, since _pdf does not restrict shape (only public method does) + c1._pdf(accepted_shape_1) + c1._pdf(accepted_shape_2) + c1._pdf(accepted_shape_3) + + +def test_uniformfloat_get_max_density(): + c1 = UniformFloatHyperparameter("param", lower=0, upper=10) + c2 = UniformFloatHyperparameter("logparam", lower=np.exp(0), upper=np.exp(10), log=True) + c3 = UniformFloatHyperparameter("param", lower=0, upper=0.5) + assert c1.get_max_density() == 0.1 + assert c2.get_max_density() == pytest.approx(4.539992976248485e-05) + assert c3.get_max_density() == 2 + + +def test_normalfloat(): + # TODO test non-equality + f1 = NormalFloatHyperparameter("param", 0.5, 10.5) + f1_ = NormalFloatHyperparameter("param", 0.5, 10.5) + assert f1 == f1_ + assert str(f1) == "param, Type: NormalFloat, Mu: 0.5 Sigma: 10.5, Default: 0.5" + + # Due to seemingly different numbers with x86_64 and i686 architectures + # we got these numbers, where last two are slightly different + # 5.715498606617943, -0.9517751622974389, + # 7.3007296500572725, 16.49181349228427 + # They are equal up to 14 decimal places + expected = [5.715498606617943, -0.9517751622974389, 7.300729650057271, 16.491813492284265] + np.testing.assert_almost_equal( + f1.get_neighbors(0.5, rs=np.random.RandomState(42)), + expected, + decimal=14, + ) + + # Test attributes are accessible + assert f1.name == "param" + assert f1.mu == pytest.approx(0.5) + assert f1.sigma == pytest.approx(10.5) + assert f1.q == pytest.approx(None) + assert f1.log is False + assert f1.default_value == pytest.approx(0.5) + assert f1.normalized_default_value == pytest.approx(0.5) + + # Test copy + copy_f1 = copy.copy(f1) + + assert copy_f1.name == f1.name + assert copy_f1.mu == f1.mu + assert copy_f1.sigma == f1.sigma + assert copy_f1.default_value == f1.default_value + + f2 = NormalFloatHyperparameter("param", 0, 10, q=0.1) + f2_ = NormalFloatHyperparameter("param", 0, 10, q=0.1) + assert f2 == f2_ + assert str(f2) == "param, Type: NormalFloat, Mu: 0.0 Sigma: 10.0, Default: 0.0, Q: 0.1" + + f3 = NormalFloatHyperparameter("param", 0, 10, log=True) + f3_ = NormalFloatHyperparameter("param", 0, 10, log=True) + assert f3 == f3_ + assert str(f3) == "param, Type: NormalFloat, Mu: 0.0 Sigma: 10.0, Default: 1.0, on log-scale" + + f4 = NormalFloatHyperparameter("param", 0, 10, default_value=1.0) + f4_ = NormalFloatHyperparameter("param", 0, 10, default_value=1.0) + assert f4 == f4_ + assert str(f4) == "param, Type: NormalFloat, Mu: 0.0 Sigma: 10.0, Default: 1.0" + + f5 = NormalFloatHyperparameter("param", 0, 10, default_value=3.0, q=0.1, log=True) + f5_ = NormalFloatHyperparameter("param", 0, 10, default_value=3.0, q=0.1, log=True) + assert f5 == f5_ + assert ( + str(f5) + == "param, Type: NormalFloat, Mu: 0.0 Sigma: 10.0, Default: 3.0, on log-scale, Q: 0.1" + ) + + assert f1 != f2 + assert f1 != "UniformFloat" + + with pytest.raises(ValueError): f6 = NormalFloatHyperparameter( "param", 5, 10, lower=0.1, - upper=10, + upper=0.1, default_value=5.0, q=0.1, log=True, ) - f6_ = NormalFloatHyperparameter( + + with pytest.raises(ValueError): + f6 = NormalFloatHyperparameter( "param", 5, 10, lower=0.1, - upper=10, default_value=5.0, q=0.1, log=True, ) - assert f6 == f6_ - assert ( - "param, Type: NormalFloat, Mu: 5.0 Sigma: 10.0, Range: [0.1, 10.0], " - + "Default: 5.0, on log-scale, Q: 0.1" - == str(f6) - ) - - # Due to seemingly different numbers with x86_64 and i686 architectures - # we got these numbers, where the first one is slightly different - # They are equal up to 14 decimal places - expected = [9.967141530112327, 3.6173569882881536, 10.0, 10.0] - np.testing.assert_almost_equal( - f6.get_neighbors(5, rs=np.random.RandomState(42)), - expected, - decimal=14, - ) - - assert f1 != f2 - assert f1 != "UniformFloat" - # test that meta-data is stored correctly - f_meta = NormalFloatHyperparameter( + with pytest.raises(ValueError): + f6 = NormalFloatHyperparameter( "param", - 0.1, + 5, 10, + upper=0.1, + default_value=5.0, q=0.1, log=True, - default_value=1.0, - meta=dict(self.meta_data), - ) - assert f_meta.meta == self.meta_data - - # Test get_size - for float_hp in (f1, f2, f3, f4, f5): - assert np.isinf(float_hp.get_size()) - assert f6.get_size() == 100 - - with pytest.raises(ValueError): - _ = NormalFloatHyperparameter("param", 5, 10, lower=0.1, upper=10, default_value=10.01) - - with pytest.raises(ValueError): - _ = NormalFloatHyperparameter("param", 5, 10, lower=0.1, upper=10, default_value=0.09) - - def test_normalfloat_to_uniformfloat(self): - f1 = NormalFloatHyperparameter("param", 0, 10, q=0.1) - f1_expected = UniformFloatHyperparameter("param", -30, 30, q=0.1) - f1_actual = f1.to_uniform() - assert f1_expected == f1_actual - - f2 = NormalFloatHyperparameter("param", 0, 10, lower=-20, upper=20, q=0.1) - f2_expected = UniformFloatHyperparameter("param", -20, 20, q=0.1) - f2_actual = f2.to_uniform() - assert f2_expected == f2_actual - - def test_normalfloat_is_legal(self): - f1 = NormalFloatHyperparameter("param", 0, 10) - assert f1.is_legal(3.0) - assert f1.is_legal(2) - assert not f1.is_legal("Hahaha") - - # Test legal vector values - assert f1.is_legal_vector(1.0) - assert f1.is_legal_vector(0.0) - assert f1.is_legal_vector(0) - assert f1.is_legal_vector(0.3) - assert f1.is_legal_vector(-0.1) - assert f1.is_legal_vector(1.1) - self.assertRaises(TypeError, f1.is_legal_vector, "Hahaha") - - f2 = NormalFloatHyperparameter("param", 5, 10, lower=0.1, upper=10, default_value=5.0) - assert f2.is_legal(5.0) - assert not f2.is_legal(10.01) - assert not f2.is_legal(0.09) - - def test_normalfloat_to_integer(self): - f1 = NormalFloatHyperparameter("param", 0, 10) - f2_expected = NormalIntegerHyperparameter("param", 0, 10) - f2_actual = f1.to_integer() - assert f2_expected == f2_actual - - def test_normalfloat_pdf(self): - c1 = NormalFloatHyperparameter("param", lower=0, upper=10, mu=3, sigma=2) - c2 = NormalFloatHyperparameter( - "logparam", - lower=np.exp(0), - upper=np.exp(10), - mu=3, - sigma=2, - log=True, - ) - c3 = NormalFloatHyperparameter("param", lower=0, upper=0.5, mu=-1, sigma=0.2) - - point_1 = np.array([3]) - point_1_log = np.array([np.exp(3)]) - point_2 = np.array([10]) - point_2_log = np.array([np.exp(10)]) - point_3 = np.array([0]) - array_1 = np.array([3, 10, 10.01]) - array_1_log = np.array([np.exp(3), np.exp(10), np.exp(10.01)]) - point_outside_range_1 = np.array([-0.01]) - point_outside_range_2 = np.array([10.01]) - point_outside_range_1_log = np.array([np.exp(-0.01)]) - point_outside_range_2_log = np.array([np.exp(10.01)]) - wrong_shape_1 = np.array([[3]]) - wrong_shape_2 = np.array([3, 5, 7]).reshape(1, -1) - wrong_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) - - self.assertAlmostEqual(c1.pdf(point_1)[0], 0.2138045617479014) - self.assertAlmostEqual(c2.pdf(point_1_log)[0], 0.2138045617479014) - self.assertAlmostEqual(c1.pdf(point_2)[0], 0.000467695579850518) - self.assertAlmostEqual(c2.pdf(point_2_log)[0], 0.000467695579850518) - self.assertAlmostEqual(c3.pdf(point_3)[0], 25.932522722334905) - # TODO - change this once the is_legal support is there - # but does not have an actual impact of now - assert c1.pdf(point_outside_range_1)[0] == 0.0 - assert c1.pdf(point_outside_range_2)[0] == 0.0 - assert c2.pdf(point_outside_range_1_log)[0] == 0.0 - assert c2.pdf(point_outside_range_2_log)[0] == 0.0 - - array_results = c1.pdf(array_1) - array_results_log = c2.pdf(array_1_log) - expected_results = np.array([0.2138045617479014, 0.0004676955798505186, 0]) - assert array_results.shape == expected_results.shape - assert array_results_log.shape == expected_results.shape - for res, log_res, exp_res in zip(array_results, array_results, expected_results): - self.assertAlmostEqual(res, exp_res) - self.assertAlmostEqual(log_res, exp_res) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1.pdf(0.2) - with self.assertRaises(TypeError): - c1.pdf("pdf") - - c_nobounds = NormalFloatHyperparameter("param", mu=3, sigma=2) - self.assertAlmostEqual(c_nobounds.pdf(np.array([2]))[0], 0.17603266338214976) - - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_1) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_2) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_3) - - def test_normalfloat__pdf(self): - c1 = NormalFloatHyperparameter("param", lower=0, upper=10, mu=3, sigma=2) - c2 = NormalFloatHyperparameter( - "logparam", - lower=np.exp(0), - upper=np.exp(10), - mu=3, - sigma=2, - log=True, - ) - c3 = NormalFloatHyperparameter("param", lower=0, upper=0.5, mu=-1, sigma=0.2) - - # since there is no logtransformation, the logged and unlogged parameters - # should output the same given the same input - - point_1 = np.array([3]) - point_2 = np.array([10]) - point_3 = np.array([0]) - array_1 = np.array([3, 10, 10.01]) - point_outside_range_1 = np.array([-0.01]) - point_outside_range_2 = np.array([10.01]) - accepted_shape_1 = np.array([[3]]) - accepted_shape_2 = np.array([3, 5, 7]).reshape(1, -1) - accepted_shape_3 = np.array([7, 5, 3]).reshape(-1, 1) - - self.assertAlmostEqual(c1._pdf(point_1)[0], 0.2138045617479014) - self.assertAlmostEqual(c2._pdf(point_1)[0], 0.2138045617479014) - self.assertAlmostEqual(c1._pdf(point_2)[0], 0.000467695579850518) - self.assertAlmostEqual(c2._pdf(point_2)[0], 0.000467695579850518) - self.assertAlmostEqual(c3._pdf(point_3)[0], 25.932522722334905) - # TODO - change this once the is_legal support is there - # but does not have an actual impact of now - assert c1._pdf(point_outside_range_1)[0] == 0.0 - assert c1._pdf(point_outside_range_2)[0] == 0.0 - assert c2._pdf(point_outside_range_1)[0] == 0.0 - assert c2._pdf(point_outside_range_2)[0] == 0.0 - - array_results = c1.pdf(array_1) - array_results_log = c2.pdf(array_1) - expected_results = np.array([0.2138045617479014, 0.0004676955798505186, 0]) - assert array_results.shape == expected_results.shape - assert array_results_log.shape == expected_results.shape - for res, log_res, exp_res in zip(array_results, array_results, expected_results): - self.assertAlmostEqual(res, exp_res) - self.assertAlmostEqual(log_res, exp_res) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1.pdf(0.2) - with self.assertRaises(TypeError): - c1.pdf("pdf") - - c_nobounds = NormalFloatHyperparameter("param", mu=3, sigma=2) - self.assertAlmostEqual(c_nobounds.pdf(np.array([2]))[0], 0.17603266338214976) - - # Simply check that it runs, since _pdf does not restrict shape (only public method does) - c1._pdf(accepted_shape_1) - c1._pdf(accepted_shape_2) - c1._pdf(accepted_shape_3) - - def test_normalfloat_get_max_density(self): - c1 = NormalFloatHyperparameter("param", lower=0, upper=10, mu=3, sigma=2) - c2 = NormalFloatHyperparameter( - "logparam", - lower=np.exp(0), - upper=np.exp(10), - mu=3, - sigma=2, - log=True, - ) - c3 = NormalFloatHyperparameter("param", lower=0, upper=0.5, mu=-1, sigma=0.2) - self.assertAlmostEqual(c1.get_max_density(), 0.2138045617479014, places=9) - self.assertAlmostEqual(c2.get_max_density(), 0.2138045617479014, places=9) - self.assertAlmostEqual(c3.get_max_density(), 25.932522722334905, places=9) - - def test_betafloat(self): - # TODO test non-equality - f1 = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.0) - f1_ = BetaFloatHyperparameter("param", lower=-2, upper=2, alpha=3, beta=1) - assert f1 == f1_ - assert ( - str(f1_) - == "param, Type: BetaFloat, Alpha: 3.0 Beta: 1.0, Range: [-2.0, 2.0], Default: 2.0" - ) - - u1 = UniformFloatHyperparameter("param", lower=0.0, upper=1.0) - b1 = BetaFloatHyperparameter("param", lower=0.0, upper=1.0, alpha=3.0, beta=1.0) - - # with identical domains, beta and uniform should sample the same points - assert u1.get_neighbors(0.5, rs=np.random.RandomState(42)) == b1.get_neighbors( - 0.5, - rs=np.random.RandomState(42), - ) - # Test copy - copy_f1 = copy.copy(f1) - assert copy_f1.name == f1.name - - f2 = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.0, q=0.1) - f2_ = BetaFloatHyperparameter("param", lower=-2, upper=2, alpha=3, beta=1, q=0.1) - assert f2 == f2_ - - assert ( - str(f2) - == "param, Type: BetaFloat, Alpha: 3.0 Beta: 1.0, Range: [-2.0, 2.0], Default: 2.0, Q: 0.1" - ) - - f3 = BetaFloatHyperparameter( - "param", - lower=10 ** (-5), - upper=10.0, - alpha=6.0, - beta=2.0, - log=True, - ) - f3_ = BetaFloatHyperparameter( - "param", - lower=10 ** (-5), - upper=10.0, - alpha=6.0, - beta=2.0, - log=True, - ) - assert f3 == f3_ - assert ( - str(f3) - == "param, Type: BetaFloat, Alpha: 6.0 Beta: 2.0, Range: [1e-05, 10.0], Default: 1.0, on log-scale" - ) - - f4 = BetaFloatHyperparameter( - "param", - lower=1, - upper=1000.0, - alpha=2.0, - beta=2.0, - log=True, - q=1.0, - ) - f4_ = BetaFloatHyperparameter( - "param", - lower=1, - upper=1000.0, - alpha=2.0, - beta=2.0, - log=True, - q=1.0, - ) - - assert f4 == f4_ - assert ( - str(f4) - == "param, Type: BetaFloat, Alpha: 2.0 Beta: 2.0, Range: [1.0, 1000.0], Default: 32.0, on log-scale, Q: 1.0" - ) - - # test that meta-data is stored correctly - f_meta = BetaFloatHyperparameter( - "param", - lower=1, - upper=10.0, - alpha=3.0, - beta=2.0, - log=False, - meta=dict(self.meta_data), ) - assert f_meta.meta == self.meta_data - with self.assertWarnsRegex( - UserWarning, + f6 = NormalFloatHyperparameter( + "param", + 5, + 10, + lower=0.1, + upper=10, + default_value=5.0, + q=0.1, + log=True, + ) + f6_ = NormalFloatHyperparameter( + "param", + 5, + 10, + lower=0.1, + upper=10, + default_value=5.0, + q=0.1, + log=True, + ) + assert f6 == f6_ + assert ( + "param, Type: NormalFloat, Mu: 5.0 Sigma: 10.0, Range: [0.1, 10.0], " + + "Default: 5.0, on log-scale, Q: 0.1" + == str(f6) + ) + + # Due to seemingly different numbers with x86_64 and i686 architectures + # we got these numbers, where the first one is slightly different + # They are equal up to 14 decimal places + expected = [9.967141530112327, 3.6173569882881536, 10.0, 10.0] + np.testing.assert_almost_equal( + f6.get_neighbors(5, rs=np.random.RandomState(42)), + expected, + decimal=14, + ) + + assert f1 != f2 + assert f1 != "UniformFloat" + + # test that meta-data is stored correctly + f_meta = NormalFloatHyperparameter( + "param", + 0.1, + 10, + q=0.1, + log=True, + default_value=1.0, + meta=dict(META_DATA), + ) + assert f_meta.meta == META_DATA + + # Test get_size + for float_hp in (f1, f2, f3, f4, f5): + assert np.isinf(float_hp.get_size()) + assert f6.get_size() == 100 + + with pytest.raises(ValueError): + _ = NormalFloatHyperparameter("param", 5, 10, lower=0.1, upper=10, default_value=10.01) + + with pytest.raises(ValueError): + _ = NormalFloatHyperparameter("param", 5, 10, lower=0.1, upper=10, default_value=0.09) + + +def test_normalfloat_to_uniformfloat(): + f1 = NormalFloatHyperparameter("param", 0, 10, q=0.1) + f1_expected = UniformFloatHyperparameter("param", -30, 30, q=0.1) + f1_actual = f1.to_uniform() + assert f1_expected == f1_actual + + f2 = NormalFloatHyperparameter("param", 0, 10, lower=-20, upper=20, q=0.1) + f2_expected = UniformFloatHyperparameter("param", -20, 20, q=0.1) + f2_actual = f2.to_uniform() + assert f2_expected == f2_actual + + +def test_normalfloat_is_legal(): + f1 = NormalFloatHyperparameter("param", 0, 10) + assert f1.is_legal(3.0) + assert f1.is_legal(2) + assert not f1.is_legal("Hahaha") + + # Test legal vector values + assert f1.is_legal_vector(1.0) + assert f1.is_legal_vector(0.0) + assert f1.is_legal_vector(0) + assert f1.is_legal_vector(0.3) + assert f1.is_legal_vector(-0.1) + assert f1.is_legal_vector(1.1) + with pytest.raises(TypeError): + f1.is_legal_vector("Hahaha") + + f2 = NormalFloatHyperparameter("param", 5, 10, lower=0.1, upper=10, default_value=5.0) + assert f2.is_legal(5.0) + assert not f2.is_legal(10.01) + assert not f2.is_legal(0.09) + + +def test_normalfloat_to_integer(): + f1 = NormalFloatHyperparameter("param", 0, 10) + f2_expected = NormalIntegerHyperparameter("param", 0, 10) + f2_actual = f1.to_integer() + assert f2_expected == f2_actual + + +def test_normalfloat_pdf(): + c1 = NormalFloatHyperparameter("param", lower=0, upper=10, mu=3, sigma=2) + c2 = NormalFloatHyperparameter( + "logparam", + lower=np.exp(0), + upper=np.exp(10), + mu=3, + sigma=2, + log=True, + ) + c3 = NormalFloatHyperparameter("param", lower=0, upper=0.5, mu=-1, sigma=0.2) + + point_1 = np.array([3]) + point_1_log = np.array([np.exp(3)]) + point_2 = np.array([10]) + point_2_log = np.array([np.exp(10)]) + point_3 = np.array([0]) + array_1 = np.array([3, 10, 10.01]) + array_1_log = np.array([np.exp(3), np.exp(10), np.exp(10.01)]) + point_outside_range_1 = np.array([-0.01]) + point_outside_range_2 = np.array([10.01]) + point_outside_range_1_log = np.array([np.exp(-0.01)]) + point_outside_range_2_log = np.array([np.exp(10.01)]) + wrong_shape_1 = np.array([[3]]) + wrong_shape_2 = np.array([3, 5, 7]).reshape(1, -1) + wrong_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) + + assert c1.pdf(point_1)[0] == pytest.approx(0.2138045617479014) + assert c2.pdf(point_1_log)[0] == pytest.approx(0.2138045617479014) + assert c1.pdf(point_2)[0] == pytest.approx(0.000467695579850518) + assert c2.pdf(point_2_log)[0] == pytest.approx(0.000467695579850518) + assert c3.pdf(point_3)[0] == pytest.approx(25.932522722334905) + # TODO - change this once the is_legal support is there + # but does not have an actual impact of now + assert c1.pdf(point_outside_range_1)[0] == 0.0 + assert c1.pdf(point_outside_range_2)[0] == 0.0 + assert c2.pdf(point_outside_range_1_log)[0] == 0.0 + assert c2.pdf(point_outside_range_2_log)[0] == 0.0 + + array_results = c1.pdf(array_1) + array_results_log = c2.pdf(array_1_log) + expected_results = np.array([0.2138045617479014, 0.0004676955798505186, 0]) + assert array_results.shape == expected_results.shape + assert array_results_log.shape == expected_results.shape + for res, log_res, exp_res in zip(array_results, array_results, expected_results): + assert res == pytest.approx(exp_res) + assert log_res == pytest.approx(exp_res) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1.pdf(0.2) + with pytest.raises(TypeError): + c1.pdf("pdf") + + c_nobounds = NormalFloatHyperparameter("param", mu=3, sigma=2) + assert c_nobounds.pdf(np.array([2]))[0] == pytest.approx(0.17603266338214976) + + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_1) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_2) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_3) + + +def test_normalfloat__pdf(): + c1 = NormalFloatHyperparameter("param", lower=0, upper=10, mu=3, sigma=2) + c2 = NormalFloatHyperparameter( + "logparam", + lower=np.exp(0), + upper=np.exp(10), + mu=3, + sigma=2, + log=True, + ) + c3 = NormalFloatHyperparameter("param", lower=0, upper=0.5, mu=-1, sigma=0.2) + + # since there is no logtransformation, the logged and unlogged parameters + # should output the same given the same input + + point_1 = np.array([3]) + point_2 = np.array([10]) + point_3 = np.array([0]) + array_1 = np.array([3, 10, 10.01]) + point_outside_range_1 = np.array([-0.01]) + point_outside_range_2 = np.array([10.01]) + accepted_shape_1 = np.array([[3]]) + accepted_shape_2 = np.array([3, 5, 7]).reshape(1, -1) + accepted_shape_3 = np.array([7, 5, 3]).reshape(-1, 1) + + assert c1._pdf(point_1)[0] == pytest.approx(0.2138045617479014) + assert c2._pdf(point_1)[0] == pytest.approx(0.2138045617479014) + assert c1._pdf(point_2)[0] == pytest.approx(0.000467695579850518) + assert c2._pdf(point_2)[0] == pytest.approx(0.000467695579850518) + assert c3._pdf(point_3)[0] == pytest.approx(25.932522722334905) + # TODO - change this once the is_legal support is there + # but does not have an actual impact of now + assert c1._pdf(point_outside_range_1)[0] == 0.0 + assert c1._pdf(point_outside_range_2)[0] == 0.0 + assert c2._pdf(point_outside_range_1)[0] == 0.0 + assert c2._pdf(point_outside_range_2)[0] == 0.0 + + array_results = c1.pdf(array_1) + array_results_log = c2.pdf(array_1) + expected_results = np.array([0.2138045617479014, 0.0004676955798505186, 0]) + assert array_results.shape == expected_results.shape + assert array_results_log.shape == expected_results.shape + for res, log_res, exp_res in zip(array_results, array_results, expected_results): + assert res == pytest.approx(exp_res) + assert log_res == pytest.approx(exp_res) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1.pdf(0.2) + with pytest.raises(TypeError): + c1.pdf("pdf") + + c_nobounds = NormalFloatHyperparameter("param", mu=3, sigma=2) + assert c_nobounds.pdf(np.array([2]))[0] == pytest.approx(0.17603266338214976) + + # Simply check that it runs, since _pdf does not restrict shape (only public method does) + c1._pdf(accepted_shape_1) + c1._pdf(accepted_shape_2) + c1._pdf(accepted_shape_3) + + +def test_normalfloat_get_max_density(): + c1 = NormalFloatHyperparameter("param", lower=0, upper=10, mu=3, sigma=2) + c2 = NormalFloatHyperparameter( + "logparam", + lower=np.exp(0), + upper=np.exp(10), + mu=3, + sigma=2, + log=True, + ) + c3 = NormalFloatHyperparameter("param", lower=0, upper=0.5, mu=-1, sigma=0.2) + assert c1.get_max_density() == pytest.approx(0.2138045617479014, abs=1e-9) + assert c2.get_max_density() == pytest.approx(0.2138045617479014, abs=1e-9) + assert c3.get_max_density() == pytest.approx(25.932522722334905, abs=1e-9) + + +def test_betafloat(): + # TODO test non-equality + f1 = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.0) + f1_ = BetaFloatHyperparameter("param", lower=-2, upper=2, alpha=3, beta=1) + assert f1 == f1_ + assert ( + str(f1_) == "param, Type: BetaFloat, Alpha: 3.0 Beta: 1.0, Range: [-2.0, 2.0], Default: 2.0" + ) + + u1 = UniformFloatHyperparameter("param", lower=0.0, upper=1.0) + b1 = BetaFloatHyperparameter("param", lower=0.0, upper=1.0, alpha=3.0, beta=1.0) + + # with identical domains, beta and uniform should sample the same points + assert u1.get_neighbors(0.5, rs=np.random.RandomState(42)) == b1.get_neighbors( + 0.5, + rs=np.random.RandomState(42), + ) + # Test copy + copy_f1 = copy.copy(f1) + assert copy_f1.name == f1.name + + f2 = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.0, q=0.1) + f2_ = BetaFloatHyperparameter("param", lower=-2, upper=2, alpha=3, beta=1, q=0.1) + assert f2 == f2_ + + assert ( + str(f2) + == "param, Type: BetaFloat, Alpha: 3.0 Beta: 1.0, Range: [-2.0, 2.0], Default: 2.0, Q: 0.1" + ) + + f3 = BetaFloatHyperparameter( + "param", + lower=10 ** (-5), + upper=10.0, + alpha=6.0, + beta=2.0, + log=True, + ) + f3_ = BetaFloatHyperparameter( + "param", + lower=10 ** (-5), + upper=10.0, + alpha=6.0, + beta=2.0, + log=True, + ) + assert f3 == f3_ + assert ( + str(f3) + == "param, Type: BetaFloat, Alpha: 6.0 Beta: 2.0, Range: [1e-05, 10.0], Default: 1.0, on log-scale" + ) + + f4 = BetaFloatHyperparameter( + "param", + lower=1, + upper=1000.0, + alpha=2.0, + beta=2.0, + log=True, + q=1.0, + ) + f4_ = BetaFloatHyperparameter( + "param", + lower=1, + upper=1000.0, + alpha=2.0, + beta=2.0, + log=True, + q=1.0, + ) + + assert f4 == f4_ + assert ( + str(f4) + == "param, Type: BetaFloat, Alpha: 2.0 Beta: 2.0, Range: [1.0, 1000.0], Default: 32.0, on log-scale, Q: 1.0" + ) + + # test that meta-data is stored correctly + f_meta = BetaFloatHyperparameter( + "param", + lower=1, + upper=10.0, + alpha=3.0, + beta=2.0, + log=False, + meta=dict(META_DATA), + ) + assert f_meta.meta == META_DATA + + with pytest.raises( + UserWarning, + match=( "Logscale and quantization together results in " "incorrect default values. We recommend specifying a default " - "value manually for this specific case.", - ): - BetaFloatHyperparameter( - "param", - lower=1, - upper=100.0, - alpha=3.0, - beta=2.0, - log=True, - q=1, - ) - - def test_betafloat_dist_parameters(self): - # This one should just be created without raising an error - corresponds to uniform dist. - BetaFloatHyperparameter("param", lower=0, upper=10.0, alpha=1, beta=1) - - # This one is not permitted as the co-domain is not finite - with self.assertRaises(ValueError): - BetaFloatHyperparameter("param", lower=0, upper=100, alpha=0.99, beta=0.99) - # And these parameters do not define a proper beta distribution whatsoever - with self.assertRaises(ValueError): - BetaFloatHyperparameter("param", lower=0, upper=100, alpha=-0.1, beta=-0.1) - - # test parameters that do not create a legit beta distribution, one at a time - with self.assertRaises(ValueError): - BetaFloatHyperparameter("param", lower=-2, upper=2, alpha=-11, beta=5) - with self.assertRaises(ValueError): - BetaFloatHyperparameter("param", lower=-2, upper=2, alpha=5, beta=-11) - - # test parameters that do not yield a finite co-domain, one at a time - with self.assertRaises(ValueError): - BetaFloatHyperparameter("param", lower=-2, upper=2, alpha=0.5, beta=11) - with self.assertRaises(ValueError): - BetaFloatHyperparameter("param", lower=-2, upper=2, alpha=11, beta=0.5) - - def test_betafloat_default_value(self): - # should default to the maximal value in the search space - f_max = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.0) - self.assertAlmostEqual(f_max.default_value, 2.0) - self.assertAlmostEqual(f_max.normalized_default_value, 1.0) - - f_max_log = BetaFloatHyperparameter( - "param", - lower=1.0, - upper=10.0, - alpha=3.0, - beta=1.0, - log=True, - ) - self.assertAlmostEqual(f_max_log.default_value, 10.0) - self.assertAlmostEqual(f_max_log.normalized_default_value, 1.0) - - # should default to the minimal value in the search space - f_min = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=1.0, beta=1.5) - self.assertAlmostEqual(f_min.default_value, -2.0) - self.assertAlmostEqual(f_min.normalized_default_value, 0.0) - - f_min_log = BetaFloatHyperparameter( - "param", - lower=1.0, - upper=10.0, - alpha=1.0, - beta=1.5, - log=True, - ) - self.assertAlmostEqual(f_min_log.default_value, 1.0) - self.assertAlmostEqual(f_min_log.normalized_default_value, 0.0) - - # Symmeric, should default to the middle - f_symm = BetaFloatHyperparameter("param", lower=5, upper=9, alpha=4.6, beta=4.6) - self.assertAlmostEqual(f_symm.default_value, 7) - self.assertAlmostEqual(f_symm.normalized_default_value, 0.5) - - # This should yield a value that's halfway towards the max in logspace - f_symm_log = BetaFloatHyperparameter( + "value manually for this specific case." + ), + ): + BetaFloatHyperparameter( "param", lower=1, - upper=np.exp(10), - alpha=4.6, - beta=4.6, - log=True, - ) - self.assertAlmostEqual(f_symm_log.default_value, np.exp(5)) - self.assertAlmostEqual(f_symm_log.normalized_default_value, 0.5) - - # Uniform, should also default to the middle - f_unif = BetaFloatHyperparameter("param", lower=2.2, upper=3.2, alpha=1.0, beta=1.0) - self.assertAlmostEqual(f_unif.default_value, 2.7) - self.assertAlmostEqual(f_unif.normalized_default_value, 0.5) - - # This should yield a value that's halfway towards the max in logspace - f_unif_log = BetaFloatHyperparameter( - "param", - lower=np.exp(2.2), - upper=np.exp(3.2), - alpha=1.0, - beta=1.0, - log=True, - ) - self.assertAlmostEqual(f_unif_log.default_value, np.exp(2.7)) - self.assertAlmostEqual(f_unif_log.normalized_default_value, 0.5) - - # Then, test a case where the default value is the mode of the beta dist - f_max = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=4.7, beta=2.12) - self.assertAlmostEqual(f_max.default_value, 1.0705394190871367) - self.assertAlmostEqual(f_max.normalized_default_value, 0.7676348547717842) - - f_max_log = BetaFloatHyperparameter( - "param", - lower=np.exp(-2.0), - upper=np.exp(2.0), - alpha=4.7, - beta=2.12, + upper=100.0, + alpha=3.0, + beta=2.0, log=True, - ) - self.assertAlmostEqual(f_max_log.default_value, np.exp(1.0705394190871367)) - self.assertAlmostEqual(f_max_log.normalized_default_value, 0.7676348547717842) - - # These parameters do not yeild an integer default solution - f_quant = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=4.7, beta=2.12, q=1) - self.assertAlmostEqual(f_quant.default_value, 1.0) - - f_log_quant = BetaFloatHyperparameter( - "param", - lower=1, - upper=100000, - alpha=2, - beta=2, q=1, - log=True, ) - self.assertAlmostEqual(f_log_quant.default_value, 316) - - # since it's quantized, it gets distributed evenly among the search space - # as such, the possible normalized defaults are 0.1, 0.3, 0.5, 0.7, 0.9 - self.assertAlmostEqual(f_quant.normalized_default_value, 0.7, places=4) - # TODO log and quantization together does not yield a correct default for the beta - # hyperparameter, but it is relatively close to being correct. However, it is not - # being - # The default value is independent of whether you log the parameter or not - f_legal_nolog = BetaFloatHyperparameter( +def test_betafloat_dist_parameters(): + # This one should just be created without raising an error - corresponds to uniform dist. + BetaFloatHyperparameter("param", lower=0, upper=10.0, alpha=1, beta=1) + + # This one is not permitted as the co-domain is not finite + with pytest.raises(ValueError): + BetaFloatHyperparameter("param", lower=0, upper=100, alpha=0.99, beta=0.99) + # And these parameters do not define a proper beta distribution whatsoever + with pytest.raises(ValueError): + BetaFloatHyperparameter("param", lower=0, upper=100, alpha=-0.1, beta=-0.1) + + # test parameters that do not create a legit beta distribution, one at a time + with pytest.raises(ValueError): + BetaFloatHyperparameter("param", lower=-2, upper=2, alpha=-11, beta=5) + with pytest.raises(ValueError): + BetaFloatHyperparameter("param", lower=-2, upper=2, alpha=5, beta=-11) + + # test parameters that do not yield a finite co-domain, one at a time + with pytest.raises(ValueError): + BetaFloatHyperparameter("param", lower=-2, upper=2, alpha=0.5, beta=11) + with pytest.raises(ValueError): + BetaFloatHyperparameter("param", lower=-2, upper=2, alpha=11, beta=0.5) + + +def test_betafloat_default_value(): + # should default to the maximal value in the search space + f_max = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.0) + assert f_max.default_value == pytest.approx(2.0) + assert f_max.normalized_default_value == pytest.approx(1.0) + + f_max_log = BetaFloatHyperparameter( + "param", + lower=1.0, + upper=10.0, + alpha=3.0, + beta=1.0, + log=True, + ) + assert f_max_log.default_value == pytest.approx(10.0) + assert f_max_log.normalized_default_value == pytest.approx(1.0) + + # should default to the minimal value in the search space + f_min = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=1.0, beta=1.5) + assert f_min.default_value == pytest.approx(-2.0) + assert f_min.normalized_default_value == pytest.approx(0.0) + + f_min_log = BetaFloatHyperparameter( + "param", + lower=1.0, + upper=10.0, + alpha=1.0, + beta=1.5, + log=True, + ) + assert f_min_log.default_value == pytest.approx(1.0) + assert f_min_log.normalized_default_value == pytest.approx(0.0) + + # Symmeric, should default to the middle + f_symm = BetaFloatHyperparameter("param", lower=5, upper=9, alpha=4.6, beta=4.6) + assert f_symm.default_value == pytest.approx(7) + assert f_symm.normalized_default_value == pytest.approx(0.5) + + # This should yield a value that's halfway towards the max in logspace + f_symm_log = BetaFloatHyperparameter( + "param", + lower=1, + upper=np.exp(10), + alpha=4.6, + beta=4.6, + log=True, + ) + assert f_symm_log.default_value == pytest.approx(np.exp(5)) + assert f_symm_log.normalized_default_value == pytest.approx(0.5) + + # Uniform, should also default to the middle + f_unif = BetaFloatHyperparameter("param", lower=2.2, upper=3.2, alpha=1.0, beta=1.0) + assert f_unif.default_value == pytest.approx(2.7) + assert f_unif.normalized_default_value == pytest.approx(0.5) + + # This should yield a value that's halfway towards the max in logspace + f_unif_log = BetaFloatHyperparameter( + "param", + lower=np.exp(2.2), + upper=np.exp(3.2), + alpha=1.0, + beta=1.0, + log=True, + ) + assert f_unif_log.default_value == pytest.approx(np.exp(2.7)) + assert f_unif_log.normalized_default_value == pytest.approx(0.5) + + # Then, test a case where the default value is the mode of the beta dist + f_max = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=4.7, beta=2.12) + assert f_max.default_value == pytest.approx(1.0705394190871367) + assert f_max.normalized_default_value == pytest.approx(0.7676348547717842) + + f_max_log = BetaFloatHyperparameter( + "param", + lower=np.exp(-2.0), + upper=np.exp(2.0), + alpha=4.7, + beta=2.12, + log=True, + ) + assert f_max_log.default_value == pytest.approx(np.exp(1.0705394190871367)) + assert f_max_log.normalized_default_value == pytest.approx(0.7676348547717842) + + # These parameters do not yeild an integer default solution + f_quant = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=4.7, beta=2.12, q=1) + assert f_quant.default_value == pytest.approx(1.0) + + f_log_quant = BetaFloatHyperparameter( + "param", + lower=1, + upper=100000, + alpha=2, + beta=2, + q=1, + log=True, + ) + assert f_log_quant.default_value == pytest.approx(316) + + # since it's quantized, it gets distributed evenly among the search space + # as such, the possible normalized defaults are 0.1, 0.3, 0.5, 0.7, 0.9 + assert f_quant.normalized_default_value == pytest.approx(0.7, abs=1e-4) + + # TODO log and quantization together does not yield a correct default for the beta + # hyperparameter, but it is relatively close to being correct. However, it is not + # being + + # The default value is independent of whether you log the parameter or not + f_legal_nolog = BetaFloatHyperparameter( + "param", + lower=1, + upper=10.0, + alpha=3.0, + beta=2.0, + default_value=1, + log=True, + ) + f_legal_log = BetaFloatHyperparameter( + "param", + lower=1, + upper=10.0, + alpha=3.0, + beta=2.0, + default_value=1, + log=False, + ) + + assert f_legal_nolog.default_value == pytest.approx(1) + assert f_legal_log.default_value == pytest.approx(1) + + # These are necessary, as we bypass the same check in the UniformFloatHP by design + with pytest.raises(ValueError, match="Illegal default value 0"): + BetaFloatHyperparameter( "param", lower=1, upper=10.0, alpha=3.0, beta=2.0, - default_value=1, - log=True, + default_value=0, + log=False, ) - f_legal_log = BetaFloatHyperparameter( + with pytest.raises(ValueError, match="Illegal default value 0"): + BetaFloatHyperparameter( "param", lower=1, - upper=10.0, + upper=1000.0, alpha=3.0, beta=2.0, - default_value=1, - log=False, - ) - - self.assertAlmostEqual(f_legal_nolog.default_value, 1) - self.assertAlmostEqual(f_legal_log.default_value, 1) - - # These are necessary, as we bypass the same check in the UniformFloatHP by design - with self.assertRaisesRegex(ValueError, "Illegal default value 0"): - BetaFloatHyperparameter( - "param", - lower=1, - upper=10.0, - alpha=3.0, - beta=2.0, - default_value=0, - log=False, - ) - with self.assertRaisesRegex(ValueError, "Illegal default value 0"): - BetaFloatHyperparameter( - "param", - lower=1, - upper=1000.0, - alpha=3.0, - beta=2.0, - default_value=0, - log=True, - ) - - def test_betafloat_to_uniformfloat(self): - f1 = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=4, beta=2, q=0.1) - f1_expected = UniformFloatHyperparameter( - "param", - lower=-2.0, - upper=2.0, - q=0.1, - default_value=1, - ) - f1_actual = f1.to_uniform() - assert f1_expected == f1_actual - - f2 = BetaFloatHyperparameter("param", lower=1, upper=1000, alpha=3, beta=2, log=True) - f2_expected = UniformFloatHyperparameter( - "param", - lower=1, - upper=1000, - log=True, - default_value=100, - ) - f2_actual = f2.to_uniform() - assert f2_expected == f2_actual - - def test_betafloat_to_integer(self): - f1 = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=4, beta=2) - f2_expected = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=4, beta=2) - f2_actual = f1.to_integer() - assert f2_expected == f2_actual - - def test_betafloat_pdf(self): - c1 = BetaFloatHyperparameter("param", lower=0, upper=10, alpha=3, beta=2) - c2 = BetaFloatHyperparameter( - "logparam", - lower=np.exp(0), - upper=np.exp(10), - alpha=3, - beta=2, + default_value=0, log=True, ) - c3 = BetaFloatHyperparameter("param", lower=0, upper=0.5, alpha=1.1, beta=25) - - point_1 = np.array([3]) - point_1_log = np.array([np.exp(3)]) - point_2 = np.array([9.9]) - point_2_log = np.array([np.exp(9.9)]) - point_3 = np.array([0.01]) - array_1 = np.array([3, 9.9, 10.01]) - array_1_log = np.array([np.exp(3), np.exp(9.9), np.exp(10.01)]) - point_outside_range_1 = np.array([-0.01]) - point_outside_range_2 = np.array([10.01]) - point_outside_range_1_log = np.array([np.exp(-0.01)]) - point_outside_range_2_log = np.array([np.exp(10.01)]) - wrong_shape_1 = np.array([[3]]) - wrong_shape_2 = np.array([3, 5, 7]).reshape(1, -1) - wrong_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) - - self.assertAlmostEqual(c1.pdf(point_1)[0], 0.07559999999999997) - self.assertAlmostEqual(c2.pdf(point_1_log)[0], 0.07559999999999997) - self.assertAlmostEqual(c1.pdf(point_2)[0], 0.011761200000000013) - self.assertAlmostEqual(c2.pdf(point_2_log)[0], 0.011761200000000013) - self.assertAlmostEqual(c3.pdf(point_3)[0], 30.262164001861198) - # TODO - change this once the is_legal support is there - # but does not have an actual impact of now - assert c1.pdf(point_outside_range_1)[0] == 0.0 - assert c1.pdf(point_outside_range_2)[0] == 0.0 - assert c2.pdf(point_outside_range_1_log)[0] == 0.0 - assert c2.pdf(point_outside_range_2_log)[0] == 0.0 - - array_results = c1.pdf(array_1) - array_results_log = c2.pdf(array_1_log) - expected_results = np.array([0.07559999999999997, 0.011761200000000013, 0]) - assert array_results.shape == expected_results.shape - assert array_results_log.shape == expected_results.shape - for res, log_res, exp_res in zip(array_results, array_results, expected_results): - self.assertAlmostEqual(res, exp_res) - self.assertAlmostEqual(log_res, exp_res) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1.pdf(0.2) - with self.assertRaises(TypeError): - c1.pdf("pdf") - - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_1) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_2) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_3) - - def test_betafloat__pdf(self): - c1 = BetaFloatHyperparameter("param", lower=0, upper=10, alpha=3, beta=2) - c2 = BetaFloatHyperparameter( - "logparam", - lower=np.exp(0), - upper=np.exp(10), - alpha=3, - beta=2, - log=True, - ) - c3 = BetaFloatHyperparameter("param", lower=0, upper=0.5, alpha=1.1, beta=25) - - point_1 = np.array([0.3]) - point_2 = np.array([0.99]) - point_3 = np.array([0.02]) - array_1 = np.array([0.3, 0.99, 1.01]) - point_outside_range_1 = np.array([-0.01]) - point_outside_range_2 = np.array([1.01]) - accepted_shape_1 = np.array([[0.3]]) - accepted_shape_2 = np.array([0.3, 0.5, 0.7]).reshape(1, -1) - accepted_shape_3 = np.array([0.7, 0.5, 0.3]).reshape(-1, 1) - - self.assertAlmostEqual(c1._pdf(point_1)[0], 0.07559999999999997) - self.assertAlmostEqual(c2._pdf(point_1)[0], 0.07559999999999997) - self.assertAlmostEqual(c1._pdf(point_2)[0], 0.011761200000000013) - self.assertAlmostEqual(c2._pdf(point_2)[0], 0.011761200000000013) - self.assertAlmostEqual(c3._pdf(point_3)[0], 30.262164001861198) - - # TODO - change this once the is_legal support is there - # but does not have an actual impact of now - assert c1._pdf(point_outside_range_1)[0] == 0.0 - assert c1._pdf(point_outside_range_2)[0] == 0.0 - assert c2._pdf(point_outside_range_1)[0] == 0.0 - assert c2._pdf(point_outside_range_2)[0] == 0.0 - - array_results = c1._pdf(array_1) - array_results_log = c2._pdf(array_1) - expected_results = np.array([0.07559999999999997, 0.011761200000000013, 0]) - assert array_results.shape == expected_results.shape - assert array_results_log.shape == expected_results.shape - for res, log_res, exp_res in zip(array_results, array_results, expected_results): - self.assertAlmostEqual(res, exp_res) - self.assertAlmostEqual(log_res, exp_res) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1.pdf(0.2) - with self.assertRaises(TypeError): - c1.pdf("pdf") - - # Simply check that it runs, since _pdf does not restrict shape (only public method does) - c1._pdf(accepted_shape_1) - c1._pdf(accepted_shape_2) - c1._pdf(accepted_shape_3) - - def test_betafloat_get_max_density(self): - c1 = BetaFloatHyperparameter("param", lower=0, upper=10, alpha=3, beta=2) - c2 = BetaFloatHyperparameter( - "logparam", - lower=np.exp(0), - upper=np.exp(10), - alpha=3, - beta=2, - log=True, - ) - c3 = BetaFloatHyperparameter("param", lower=0, upper=0.5, alpha=1.1, beta=25) - self.assertAlmostEqual(c1.get_max_density(), 0.17777777777777776) - self.assertAlmostEqual(c2.get_max_density(), 0.17777777777777776) - self.assertAlmostEqual(c3.get_max_density(), 38.00408137865127) - - def test_uniforminteger(self): - # TODO: rounding or converting or error message? - - f1 = UniformIntegerHyperparameter("param", 0.0, 5.0) - f1_ = UniformIntegerHyperparameter("param", 0, 5) - assert f1 == f1_ - assert str(f1) == "param, Type: UniformInteger, Range: [0, 5], Default: 2" - - # Test name is accessible - assert f1.name == "param" - assert f1.lower == 0 - assert f1.upper == 5 - assert f1.q is None - assert f1.default_value == 2 - assert f1.log is False - self.assertAlmostEqual(f1.normalized_default_value, (2.0 + 0.49999) / (5.49999 + 0.49999)) - - quantization_warning = ( - "Setting quantization < 1 for Integer Hyperparameter 'param' has no effect" - ) - with pytest.warns(UserWarning, match=quantization_warning): - f2 = UniformIntegerHyperparameter("param", 0, 10, q=0.1) - with pytest.warns(UserWarning, match=quantization_warning): - f2_ = UniformIntegerHyperparameter("param", 0, 10, q=0.1) - assert f2 == f2_ - assert str(f2) == "param, Type: UniformInteger, Range: [0, 10], Default: 5" - - f2_large_q = UniformIntegerHyperparameter("param", 0, 10, q=2) - f2_large_q_ = UniformIntegerHyperparameter("param", 0, 10, q=2) - assert f2_large_q == f2_large_q_ - assert str(f2_large_q) == "param, Type: UniformInteger, Range: [0, 10], Default: 5, Q: 2" - - f3 = UniformIntegerHyperparameter("param", 1, 10, log=True) - f3_ = UniformIntegerHyperparameter("param", 1, 10, log=True) - assert f3 == f3_ - assert str(f3) == "param, Type: UniformInteger, Range: [1, 10], Default: 3, on log-scale" - - f4 = UniformIntegerHyperparameter("param", 1, 10, default_value=1, log=True) - f4_ = UniformIntegerHyperparameter("param", 1, 10, default_value=1, log=True) - assert f4 == f4_ - assert str(f4) == "param, Type: UniformInteger, Range: [1, 10], Default: 1, on log-scale" - - with pytest.warns(UserWarning, match=quantization_warning): - f5 = UniformIntegerHyperparameter("param", 1, 10, default_value=1, q=0.1, log=True) - with pytest.warns(UserWarning, match=quantization_warning): - f5_ = UniformIntegerHyperparameter("param", 1, 10, default_value=1, q=0.1, log=True) - assert f5 == f5_ - assert str(f5) == "param, Type: UniformInteger, Range: [1, 10], Default: 1, on log-scale" - - assert f1 != "UniformFloat" - - # test that meta-data is stored correctly - with pytest.warns(UserWarning, match=quantization_warning): - f_meta = UniformIntegerHyperparameter( - "param", - 1, - 10, - q=0.1, - log=True, - default_value=1, - meta=dict(self.meta_data), - ) - assert f_meta.meta == self.meta_data - - assert f1.get_size() == 6 - assert f2.get_size() == 11 - assert f2_large_q.get_size() == 6 - assert f3.get_size() == 10 - assert f4.get_size() == 10 - assert f5.get_size() == 10 - - def test_uniformint_legal_float_values(self): - n_iter = UniformIntegerHyperparameter("n_iter", 5.0, 1000.0, default_value=20.0) - - assert isinstance(n_iter.default_value, int) - self.assertRaisesRegex( - ValueError, - r"For the Integer parameter n_iter, " - r"the value must be an Integer, too." - r" Right now it is a <(type|class) " - r"'float'>" - r" with value 20.5.", - UniformIntegerHyperparameter, - "n_iter", - 5.0, - 1000.0, - default_value=20.5, - ) - def test_uniformint_illegal_bounds(self): - self.assertRaisesRegex( - ValueError, - r"Negative lower bound \(0\) for log-scale hyperparameter " r"param is forbidden.", - UniformIntegerHyperparameter, - "param", - 0, - 10, - log=True, - ) - self.assertRaisesRegex( - ValueError, - "Upper bound 1 must be larger than lower bound 0 for " "hyperparameter param", - UniformIntegerHyperparameter, +def test_betafloat_to_uniformfloat(): + f1 = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=4, beta=2, q=0.1) + f1_expected = UniformFloatHyperparameter( + "param", + lower=-2.0, + upper=2.0, + q=0.1, + default_value=1, + ) + f1_actual = f1.to_uniform() + assert f1_expected == f1_actual + + f2 = BetaFloatHyperparameter("param", lower=1, upper=1000, alpha=3, beta=2, log=True) + f2_expected = UniformFloatHyperparameter( + "param", + lower=1, + upper=1000, + log=True, + default_value=100, + ) + f2_actual = f2.to_uniform() + assert f2_expected == f2_actual + + +def test_betafloat_to_integer(): + f1 = BetaFloatHyperparameter("param", lower=-2.0, upper=2.0, alpha=4, beta=2) + f2_expected = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=4, beta=2) + f2_actual = f1.to_integer() + assert f2_expected == f2_actual + + +def test_betafloat_pdf(): + c1 = BetaFloatHyperparameter("param", lower=0, upper=10, alpha=3, beta=2) + c2 = BetaFloatHyperparameter( + "logparam", + lower=np.exp(0), + upper=np.exp(10), + alpha=3, + beta=2, + log=True, + ) + c3 = BetaFloatHyperparameter("param", lower=0, upper=0.5, alpha=1.1, beta=25) + + point_1 = np.array([3]) + point_1_log = np.array([np.exp(3)]) + point_2 = np.array([9.9]) + point_2_log = np.array([np.exp(9.9)]) + point_3 = np.array([0.01]) + array_1 = np.array([3, 9.9, 10.01]) + array_1_log = np.array([np.exp(3), np.exp(9.9), np.exp(10.01)]) + point_outside_range_1 = np.array([-0.01]) + point_outside_range_2 = np.array([10.01]) + point_outside_range_1_log = np.array([np.exp(-0.01)]) + point_outside_range_2_log = np.array([np.exp(10.01)]) + wrong_shape_1 = np.array([[3]]) + wrong_shape_2 = np.array([3, 5, 7]).reshape(1, -1) + wrong_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) + + assert c1.pdf(point_1)[0] == pytest.approx(0.07559999999999997) + assert c2.pdf(point_1_log)[0] == pytest.approx(0.07559999999999997) + assert c1.pdf(point_2)[0] == pytest.approx(0.011761200000000013) + assert c2.pdf(point_2_log)[0] == pytest.approx(0.011761200000000013) + assert c3.pdf(point_3)[0] == pytest.approx(30.262164001861198) + # TODO - change this once the is_legal support is there + # but does not have an actual impact of now + assert c1.pdf(point_outside_range_1)[0] == 0.0 + assert c1.pdf(point_outside_range_2)[0] == 0.0 + assert c2.pdf(point_outside_range_1_log)[0] == 0.0 + assert c2.pdf(point_outside_range_2_log)[0] == 0.0 + + array_results = c1.pdf(array_1) + array_results_log = c2.pdf(array_1_log) + expected_results = np.array([0.07559999999999997, 0.011761200000000013, 0]) + assert array_results.shape == expected_results.shape + assert array_results_log.shape == expected_results.shape + for res, log_res, exp_res in zip(array_results, array_results, expected_results): + assert res == pytest.approx(exp_res) + assert log_res == pytest.approx(exp_res) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1.pdf(0.2) + with pytest.raises(TypeError): + c1.pdf("pdf") + + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_1) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_2) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_3) + + +def test_betafloat__pdf(): + c1 = BetaFloatHyperparameter("param", lower=0, upper=10, alpha=3, beta=2) + c2 = BetaFloatHyperparameter( + "logparam", + lower=np.exp(0), + upper=np.exp(10), + alpha=3, + beta=2, + log=True, + ) + c3 = BetaFloatHyperparameter("param", lower=0, upper=0.5, alpha=1.1, beta=25) + + point_1 = np.array([0.3]) + point_2 = np.array([0.99]) + point_3 = np.array([0.02]) + array_1 = np.array([0.3, 0.99, 1.01]) + point_outside_range_1 = np.array([-0.01]) + point_outside_range_2 = np.array([1.01]) + accepted_shape_1 = np.array([[0.3]]) + accepted_shape_2 = np.array([0.3, 0.5, 0.7]).reshape(1, -1) + accepted_shape_3 = np.array([0.7, 0.5, 0.3]).reshape(-1, 1) + + assert c1._pdf(point_1)[0] == pytest.approx(0.07559999999999997) + assert c2._pdf(point_1)[0] == pytest.approx(0.07559999999999997) + assert c1._pdf(point_2)[0] == pytest.approx(0.011761200000000013) + assert c2._pdf(point_2)[0] == pytest.approx(0.011761200000000013) + assert c3._pdf(point_3)[0] == pytest.approx(30.262164001861198) + + # TODO - change this once the is_legal support is there + # but does not have an actual impact of now + assert c1._pdf(point_outside_range_1)[0] == 0.0 + assert c1._pdf(point_outside_range_2)[0] == 0.0 + assert c2._pdf(point_outside_range_1)[0] == 0.0 + assert c2._pdf(point_outside_range_2)[0] == 0.0 + + array_results = c1._pdf(array_1) + array_results_log = c2._pdf(array_1) + expected_results = np.array([0.07559999999999997, 0.011761200000000013, 0]) + assert array_results.shape == expected_results.shape + assert array_results_log.shape == expected_results.shape + for res, log_res, exp_res in zip(array_results, array_results, expected_results): + assert res == pytest.approx(exp_res) + assert log_res == pytest.approx(exp_res) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1.pdf(0.2) + with pytest.raises(TypeError): + c1.pdf("pdf") + + # Simply check that it runs, since _pdf does not restrict shape (only public method does) + c1._pdf(accepted_shape_1) + c1._pdf(accepted_shape_2) + c1._pdf(accepted_shape_3) + + +def test_betafloat_get_max_density(): + c1 = BetaFloatHyperparameter("param", lower=0, upper=10, alpha=3, beta=2) + c2 = BetaFloatHyperparameter( + "logparam", + lower=np.exp(0), + upper=np.exp(10), + alpha=3, + beta=2, + log=True, + ) + c3 = BetaFloatHyperparameter("param", lower=0, upper=0.5, alpha=1.1, beta=25) + assert c1.get_max_density() == pytest.approx(0.17777777777777776) + assert c2.get_max_density() == pytest.approx(0.17777777777777776) + assert c3.get_max_density() == pytest.approx(38.00408137865127) + + +def test_uniforminteger(): + # TODO: rounding or converting or error message? + + f1 = UniformIntegerHyperparameter("param", 0.0, 5.0) + f1_ = UniformIntegerHyperparameter("param", 0, 5) + assert f1 == f1_ + assert str(f1) == "param, Type: UniformInteger, Range: [0, 5], Default: 2" + + # Test name is accessible + assert f1.name == "param" + assert f1.lower == 0 + assert f1.upper == 5 + assert f1.q is None + assert f1.default_value == 2 + assert f1.log is False + assert f1.normalized_default_value == pytest.approx((2.0 + 0.49999) / (5.49999 + 0.49999)) + + quantization_warning = ( + "Setting quantization < 1 for Integer Hyperparameter 'param' has no effect" + ) + with pytest.warns(UserWarning, match=quantization_warning): + f2 = UniformIntegerHyperparameter("param", 0, 10, q=0.1) + with pytest.warns(UserWarning, match=quantization_warning): + f2_ = UniformIntegerHyperparameter("param", 0, 10, q=0.1) + assert f2 == f2_ + assert str(f2) == "param, Type: UniformInteger, Range: [0, 10], Default: 5" + + f2_large_q = UniformIntegerHyperparameter("param", 0, 10, q=2) + f2_large_q_ = UniformIntegerHyperparameter("param", 0, 10, q=2) + assert f2_large_q == f2_large_q_ + assert str(f2_large_q) == "param, Type: UniformInteger, Range: [0, 10], Default: 5, Q: 2" + + f3 = UniformIntegerHyperparameter("param", 1, 10, log=True) + f3_ = UniformIntegerHyperparameter("param", 1, 10, log=True) + assert f3 == f3_ + assert str(f3) == "param, Type: UniformInteger, Range: [1, 10], Default: 3, on log-scale" + + f4 = UniformIntegerHyperparameter("param", 1, 10, default_value=1, log=True) + f4_ = UniformIntegerHyperparameter("param", 1, 10, default_value=1, log=True) + assert f4 == f4_ + assert str(f4) == "param, Type: UniformInteger, Range: [1, 10], Default: 1, on log-scale" + + with pytest.warns(UserWarning, match=quantization_warning): + f5 = UniformIntegerHyperparameter("param", 1, 10, default_value=1, q=0.1, log=True) + with pytest.warns(UserWarning, match=quantization_warning): + f5_ = UniformIntegerHyperparameter("param", 1, 10, default_value=1, q=0.1, log=True) + assert f5 == f5_ + assert str(f5) == "param, Type: UniformInteger, Range: [1, 10], Default: 1, on log-scale" + + assert f1 != "UniformFloat" + + # test that meta-data is stored correctly + with pytest.warns(UserWarning, match=quantization_warning): + f_meta = UniformIntegerHyperparameter( "param", 1, - 0, - ) - - def test_uniformint_pdf(self): - c1 = UniformIntegerHyperparameter("param", lower=0, upper=4) - c2 = UniformIntegerHyperparameter("logparam", lower=1, upper=10000, log=True) - c3 = UniformIntegerHyperparameter("param", lower=-1, upper=12) - point_1 = np.array([0]) - point_1_log = np.array([1]) - point_2 = np.array([3.0]) - point_2_log = np.array([3.0]) - non_integer_point = np.array([3.7]) - array_1 = np.array([1, 3, 3.7]) - point_outside_range = np.array([-1]) - point_outside_range_log = np.array([10001]) - wrong_shape_1 = np.array([[3]]) - wrong_shape_2 = np.array([3, 5, 7]).reshape(1, -1) - wrong_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) - - # need to lower the amount of places since the bounds - # are inexact (._lower=-0.49999, ._upper=4.49999) - self.assertAlmostEqual(c1.pdf(point_1)[0], 0.2, places=5) - self.assertAlmostEqual(c2.pdf(point_1_log)[0], 0.0001, places=5) - self.assertAlmostEqual(c1.pdf(point_2)[0], 0.2, places=5) - self.assertAlmostEqual(c2.pdf(point_2_log)[0], 0.0001, places=5) - self.assertAlmostEqual(c1.pdf(non_integer_point)[0], 0.0, places=5) - self.assertAlmostEqual(c2.pdf(non_integer_point)[0], 0.0, places=5) - self.assertAlmostEqual(c3.pdf(point_1)[0], 0.07142857142857142, places=5) - - # TODO - change this once the is_legal support is there - # but does not have an actual impact of now - # since inverse_transform pulls everything into range, - # even points outside get evaluated in range - self.assertAlmostEqual(c1.pdf(point_outside_range)[0], 0.2, places=5) - self.assertAlmostEqual(c2.pdf(point_outside_range_log)[0], 0.0001, places=5) - - # this, however, is a negative value on a log param, which cannot be pulled into range - with pytest.warns(RuntimeWarning, match="invalid value encountered in log"): - assert c2.pdf(point_outside_range)[0] == 0.0 - - array_results = c1.pdf(array_1) - array_results_log = c2.pdf(array_1) - expected_results = np.array([0.2, 0.2, 0]) - expected_results_log = np.array([0.0001, 0.0001, 0]) - self.assertAlmostEqual(array_results.shape, expected_results.shape) - assert array_results_log.shape == expected_results.shape - for res, log_res, exp_res, _ in zip( - array_results, - array_results, - expected_results, - expected_results_log, - ): - self.assertAlmostEqual(res, exp_res, places=5) - self.assertAlmostEqual(log_res, exp_res, places=5) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1.pdf(0.2) - with self.assertRaises(TypeError): - c1.pdf("pdf") - - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_1) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_2) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_3) - - def test_uniformint__pdf(self): - c1 = UniformIntegerHyperparameter("param", lower=0, upper=4) - c2 = UniformIntegerHyperparameter("logparam", lower=1, upper=10000, log=True) - - point_1 = np.array([0]) - point_2 = np.array([0.7]) - array_1 = np.array([0, 0.7, 1.1]) - point_outside_range = np.array([-0.1]) - accepted_shape_1 = np.array([[0.7]]) - accepted_shape_2 = np.array([0, 0.7, 1.1]).reshape(1, -1) - accepted_shape_3 = np.array([1.1, 0.7, 0]).reshape(-1, 1) - - # need to lower the amount of places since the bounds - # are inexact (._lower=-0.49999, ._upper=4.49999) - self.assertAlmostEqual(c1._pdf(point_1)[0], 0.2, places=5) - self.assertAlmostEqual(c2._pdf(point_1)[0], 0.0001, places=5) - self.assertAlmostEqual(c1._pdf(point_2)[0], 0.2, places=5) - self.assertAlmostEqual(c2._pdf(point_2)[0], 0.0001, places=5) - - # TODO - change this once the is_legal support is there - # but does not have an actual impact of now - # since inverse_transform pulls everything into range, - # even points outside get evaluated in range - self.assertAlmostEqual(c1._pdf(point_outside_range)[0], 0.0, places=5) - self.assertAlmostEqual(c2._pdf(point_outside_range)[0], 0.0, places=5) - - array_results = c1._pdf(array_1) - array_results_log = c2._pdf(array_1) - expected_results = np.array([0.2, 0.2, 0]) - expected_results_log = np.array([0.0001, 0.0001, 0]) - self.assertAlmostEqual(array_results.shape, expected_results.shape) - assert array_results_log.shape == expected_results.shape - for res, log_res, exp_res, exp_log_res in zip( - array_results, - array_results_log, - expected_results, - expected_results_log, - ): - self.assertAlmostEqual(res, exp_res, places=5) - self.assertAlmostEqual(log_res, exp_log_res, places=5) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1._pdf(0.2) - with self.assertRaises(TypeError): - c1._pdf("pdf") - - # Simply check that it runs, since _pdf does not restrict shape (only public method does) - c1._pdf(accepted_shape_1) - c1._pdf(accepted_shape_2) - c1._pdf(accepted_shape_3) - - def test_uniformint_get_max_density(self): - c1 = UniformIntegerHyperparameter("param", lower=0, upper=4) - c2 = UniformIntegerHyperparameter("logparam", lower=1, upper=10000, log=True) - c3 = UniformIntegerHyperparameter("param", lower=-1, upper=12) - self.assertAlmostEqual(c1.get_max_density(), 0.2) - self.assertAlmostEqual(c2.get_max_density(), 0.0001) - self.assertAlmostEqual(c3.get_max_density(), 0.07142857142857142) - - def test_uniformint_get_neighbors(self): - rs = np.random.RandomState(seed=1) - for i_upper in range(1, 10): - c1 = UniformIntegerHyperparameter("param", lower=0, upper=i_upper) - for i_value in range(0, i_upper + 1): - float_value = c1._inverse_transform(np.array([i_value]))[0] - neighbors = c1.get_neighbors(float_value, rs, number=i_upper, transform=True) - assert set(neighbors) == set(range(i_upper + 1)) - {i_value} - - def test_normalint(self): - # TODO test for unequal! - f1 = NormalIntegerHyperparameter("param", 0.5, 5.5) - f1_ = NormalIntegerHyperparameter("param", 0.5, 5.5) - assert f1 == f1_ - assert str(f1) == "param, Type: NormalInteger, Mu: 0.5 Sigma: 5.5, Default: 0.5" - - # Test attributes are accessible - assert f1.name == "param" - assert f1.mu == 0.5 - assert f1.sigma == 5.5 - assert f1.q is None - assert f1.log is False - self.assertAlmostEqual(f1.default_value, 0.5) - self.assertAlmostEqual(f1.normalized_default_value, 0.5) - - with pytest.warns( - UserWarning, - match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", - ): - f2 = NormalIntegerHyperparameter("param", 0, 10, q=0.1) - with pytest.warns( - UserWarning, - match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", - ): - f2_ = NormalIntegerHyperparameter("param", 0, 10, q=0.1) - assert f2 == f2_ - assert str(f2) == "param, Type: NormalInteger, Mu: 0 Sigma: 10, Default: 0" - - f2_large_q = NormalIntegerHyperparameter("param", 0, 10, q=2) - f2_large_q_ = NormalIntegerHyperparameter("param", 0, 10, q=2) - assert f2_large_q == f2_large_q_ - assert str(f2_large_q) == "param, Type: NormalInteger, Mu: 0 Sigma: 10, Default: 0, Q: 2" - - f3 = NormalIntegerHyperparameter("param", 0, 10, log=True) - f3_ = NormalIntegerHyperparameter("param", 0, 10, log=True) - assert f3 == f3_ - assert str(f3) == "param, Type: NormalInteger, Mu: 0 Sigma: 10, Default: 1, on log-scale" - - f4 = NormalIntegerHyperparameter("param", 0, 10, default_value=3, log=True) - f4_ = NormalIntegerHyperparameter("param", 0, 10, default_value=3, log=True) - assert f4 == f4_ - assert str(f4) == "param, Type: NormalInteger, Mu: 0 Sigma: 10, Default: 3, on log-scale" - - with pytest.warns( - UserWarning, - match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", - ): - f5 = NormalIntegerHyperparameter("param", 0, 10, q=0.1, log=True) - f5_ = NormalIntegerHyperparameter("param", 0, 10, q=0.1, log=True) - assert f5 == f5_ - assert str(f5) == "param, Type: NormalInteger, Mu: 0 Sigma: 10, Default: 1, on log-scale" - - assert f1 != f2 - assert f1 != "UniformFloat" - - # test that meta-data is stored correctly - f_meta = NormalIntegerHyperparameter( - "param", - 0, 10, - default_value=1, + q=0.1, log=True, - meta=dict(self.meta_data), - ) - assert f_meta.meta == self.meta_data - - # Test get_size - for int_hp in (f1, f2, f3, f4, f5): - assert np.isinf(int_hp.get_size()) - - # Unbounded case - f1 = NormalIntegerHyperparameter("param", 0, 10, q=1) - assert f1.get_neighbors(2, np.random.RandomState(9001), number=1) == [1] - assert f1.get_neighbors(2, np.random.RandomState(9001), number=5) == [0, 1, 9, 16, -1] - - # Bounded case - f1 = NormalIntegerHyperparameter("param", 0, 10, q=1, lower=-100, upper=100) - assert f1.get_neighbors(2, np.random.RandomState(9001), number=1) == [-11] - assert f1.get_neighbors(2, np.random.RandomState(9001), number=5) == [4, 11, 12, 15, -11] - - # Bounded case with default value out of bounds - with pytest.raises(ValueError): - _ = NormalIntegerHyperparameter("param", 5, 10, lower=1, upper=10, default_value=11) - - with pytest.raises(ValueError): - _ = NormalIntegerHyperparameter("param", 5, 10, lower=1, upper=10, default_value=0) - - def test_normalint_legal_float_values(self): - n_iter = NormalIntegerHyperparameter("n_iter", 0, 1.0, default_value=2.0) - assert isinstance(n_iter.default_value, int) - self.assertRaisesRegex( - ValueError, - r"For the Integer parameter n_iter, " - r"the value must be an Integer, too." - r" Right now it is a " - r"<(type|class) 'float'>" - r" with value 0.5.", - UniformIntegerHyperparameter, - "n_iter", - 0, - 1.0, - default_value=0.5, - ) - - def test_normalint_to_uniform(self): - with pytest.warns( - UserWarning, - match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", - ): - f1 = NormalIntegerHyperparameter("param", 0, 10, q=0.1) - f1_expected = UniformIntegerHyperparameter("param", -30, 30) - f1_actual = f1.to_uniform() - assert f1_expected == f1_actual - - def test_normalint_is_legal(self): - with pytest.warns( - UserWarning, - match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", - ): - f1 = NormalIntegerHyperparameter("param", 0, 10, q=0.1, log=True) - assert not f1.is_legal(3.1) - assert not f1.is_legal(3.0) # 3.0 behaves like an Integer - assert not f1.is_legal("BlaBlaBla") - assert f1.is_legal(2) - assert f1.is_legal(-15) - - # Test is legal vector - assert f1.is_legal_vector(1.0) - assert f1.is_legal_vector(0.0) - assert f1.is_legal_vector(0) - assert f1.is_legal_vector(0.3) - assert f1.is_legal_vector(-0.1) - assert f1.is_legal_vector(1.1) - self.assertRaises(TypeError, f1.is_legal_vector, "Hahaha") - - f2 = NormalIntegerHyperparameter("param", 5, 10, lower=1, upper=10, default_value=5) - assert f2.is_legal(5) - assert not f2.is_legal(0) - assert not f2.is_legal(11) - - def test_normalint_pdf(self): - c1 = NormalIntegerHyperparameter("param", lower=0, upper=10, mu=3, sigma=2) - c2 = NormalIntegerHyperparameter("logparam", lower=1, upper=1000, mu=3, sigma=2, log=True) - c3 = NormalIntegerHyperparameter("param", lower=0, upper=2, mu=-1.2, sigma=0.5) - - point_1 = np.array([3]) - point_1_log = np.array([10]) - point_2 = np.array([10]) - point_2_log = np.array([1000]) - point_3 = np.array([0]) - array_1 = np.array([3, 10, 11]) - array_1_log = np.array([10, 570, 1001]) - point_outside_range_1 = np.array([-1]) - point_outside_range_2 = np.array([11]) - point_outside_range_1_log = np.array([0]) - point_outside_range_2_log = np.array([1001]) - non_integer_point = np.array([5.7]) - wrong_shape_1 = np.array([[3]]) - wrong_shape_2 = np.array([3, 5, 7]).reshape(1, -1) - wrong_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) - - self.assertAlmostEqual(c1.pdf(point_1)[0], 0.20747194595587332) - self.assertAlmostEqual(c2.pdf(point_1_log)[0], 0.002625781612612434) - self.assertAlmostEqual(c1.pdf(point_2)[0], 0.00045384303905059246) - self.assertAlmostEqual(c2.pdf(point_2_log)[0], 0.0004136885586376241) - self.assertAlmostEqual(c3.pdf(point_3)[0], 0.9988874412972069) - # TODO - change this once the is_legal support is there - # but does not have an actual impact of now - assert c1.pdf(point_outside_range_1)[0] == 0.0 - assert c1.pdf(point_outside_range_2)[0] == 0.0 - with pytest.warns(RuntimeWarning, match="divide by zero encountered in log"): - assert c2.pdf(point_outside_range_1_log)[0] == 0.0 - assert c2.pdf(point_outside_range_2_log)[0] == 0.0 - - assert c1.pdf(non_integer_point)[0] == 0.0 - assert c2.pdf(non_integer_point)[0] == 0.0 - - array_results = c1.pdf(array_1) - array_results_log = c2.pdf(array_1_log) - expected_results = np.array([0.20747194595587332, 0.00045384303905059246, 0]) - expected_results_log = np.array([0.002625781612612434, 0.000688676747843256, 0]) - assert array_results.shape == expected_results.shape - assert array_results_log.shape == expected_results.shape - for res, log_res, exp_res, exp_log_res in zip( - array_results, - array_results_log, - expected_results, - expected_results_log, - ): - self.assertAlmostEqual(res, exp_res) - self.assertAlmostEqual(log_res, exp_log_res) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1.pdf(0.2) - with self.assertRaises(TypeError): - c1.pdf("pdf") - - c_nobounds = NormalFloatHyperparameter("param", mu=3, sigma=2) - self.assertAlmostEqual(c_nobounds.pdf(np.array([2]))[0], 0.17603266338214976) - - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_1) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_2) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_3) - - def test_normalint__pdf(self): - c1 = NormalIntegerHyperparameter("param", lower=0, upper=10, mu=3, sigma=2) - c2 = NormalIntegerHyperparameter("logparam", lower=1, upper=1000, mu=3, sigma=2, log=True) - - point_1 = np.array([3]) - point_2 = np.array([5.2]) - array_1 = np.array([3, 5.2, 11]) - point_outside_range_1 = np.array([-1]) - point_outside_range_2 = np.array([11]) - - self.assertAlmostEqual(c1._pdf(point_1)[0], 0.20747194595587332) - self.assertAlmostEqual(c2._pdf(point_1)[0], 0.0027903779510164133) - self.assertAlmostEqual(c1._pdf(point_2)[0], 0.1132951239316783) - self.assertAlmostEqual(c2._pdf(point_2)[0], 0.001523754039709375) - # TODO - change this once the is_legal support is there - # but does not have an actual impact of now - assert c1._pdf(point_outside_range_1)[0] == 0.0 - assert c1._pdf(point_outside_range_2)[0] == 0.0 - assert c2._pdf(point_outside_range_1)[0] == 0.0 - assert c2._pdf(point_outside_range_2)[0] == 0.0 - - array_results = c1._pdf(array_1) - array_results_log = c2._pdf(array_1) - expected_results = np.array([0.20747194595587332, 0.1132951239316783, 0]) - expected_results_log = np.array([0.0027903779510164133, 0.001523754039709375, 0]) - assert array_results.shape == expected_results.shape - assert array_results_log.shape == expected_results.shape - for res, log_res, exp_res, exp_log_res in zip( - array_results, - array_results_log, - expected_results, - expected_results_log, - ): - self.assertAlmostEqual(res, exp_res) - self.assertAlmostEqual(log_res, exp_log_res) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1.pdf(0.2) - with self.assertRaises(TypeError): - c1.pdf("pdf") - - c_nobounds = NormalFloatHyperparameter("param", mu=3, sigma=2) - self.assertAlmostEqual(c_nobounds.pdf(np.array([2]))[0], 0.17603266338214976) - - def test_normalint_get_max_density(self): - c1 = NormalIntegerHyperparameter("param", lower=0, upper=10, mu=3, sigma=2) - c2 = NormalIntegerHyperparameter("logparam", lower=1, upper=1000, mu=3, sigma=2, log=True) - c3 = NormalIntegerHyperparameter("param", lower=0, upper=2, mu=-1.2, sigma=0.5) - self.assertAlmostEqual(c1.get_max_density(), 0.20747194595587332) - self.assertAlmostEqual(c2.get_max_density(), 0.002790371598208875) - self.assertAlmostEqual(c3.get_max_density(), 0.9988874412972069) - - def test_normalint_compute_normalization(self): - ARANGE_CHUNKSIZE = 10_000_000 - lower, upper = 1, ARANGE_CHUNKSIZE * 2 - - c = NormalIntegerHyperparameter("c", mu=10, sigma=500, lower=lower, upper=upper) - chunks = arange_chunked(lower, upper, chunk_size=ARANGE_CHUNKSIZE) - # exact computation over the complete range - N = sum(c.nfhp.pdf(chunk).sum() for chunk in chunks) - self.assertAlmostEqual(c.normalization_constant, N, places=5) - - ############################################################ - def test_betaint(self): - # TODO test non-equality - f1 = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.1) - f1_ = BetaIntegerHyperparameter("param", lower=-2, upper=2, alpha=3, beta=1.1) - assert f1 == f1_ - assert ( - str(f1) == "param, Type: BetaInteger, Alpha: 3.0 Beta: 1.1, Range: [-2, 2], Default: 2" - ) - - self.assertAlmostEqual(f1.alpha, 3.0) - self.assertAlmostEqual(f1.beta, 1.1) - - # Test copy - copy_f1 = copy.copy(f1) - assert copy_f1.name == f1.name - assert copy_f1.alpha == f1.alpha - assert copy_f1.beta == f1.beta - assert copy_f1.default_value == f1.default_value - - f2 = BetaIntegerHyperparameter("param", lower=-2.0, upper=4.0, alpha=3.0, beta=1.1, q=2) - f2_ = BetaIntegerHyperparameter("param", lower=-2, upper=4, alpha=3, beta=1.1, q=2) - assert f2 == f2_ - - assert ( - str(f2) - == "param, Type: BetaInteger, Alpha: 3.0 Beta: 1.1, Range: [-2, 4], Default: 4, Q: 2" - ) - - f3 = BetaIntegerHyperparameter("param", lower=1, upper=1000, alpha=3.0, beta=2.0, log=True) - f3_ = BetaIntegerHyperparameter("param", lower=1, upper=1000, alpha=3.0, beta=2.0, log=True) - assert f3 == f3_ - assert ( - str(f3) - == "param, Type: BetaInteger, Alpha: 3.0 Beta: 2.0, Range: [1, 1000], Default: 100, on log-scale" - ) - - with self.assertRaises(ValueError): - BetaIntegerHyperparameter("param", lower=-1, upper=10.0, alpha=6.0, beta=2.0, log=True) - - # test that meta-data is stored correctly - f_meta = BetaFloatHyperparameter( + default_value=1, + meta=dict(META_DATA), + ) + assert f_meta.meta == META_DATA + + assert f1.get_size() == 6 + assert f2.get_size() == 11 + assert f2_large_q.get_size() == 6 + assert f3.get_size() == 10 + assert f4.get_size() == 10 + assert f5.get_size() == 10 + + +def test_uniformint_legal_float_values(): + n_iter = UniformIntegerHyperparameter("n_iter", 5.0, 1000.0, default_value=20.0) + + assert isinstance(n_iter.default_value, int) + with pytest.raises(ValueError, + match=r"For the Integer parameter n_iter, " + r"the value must be an Integer, too." + r" Right now it is a <(type|class) " + r"'float'>" + r" with value 20.5.", + ): + _ = UniformIntegerHyperparameter("n_iter", 5.0, 1000.0, default_value=20.5) + + +def test_uniformint_illegal_bounds(): + with pytest.raises( + ValueError, + match=r"Negative lower bound \(0\) for log-scale hyperparameter " r"param is forbidden.", + ): + UniformIntegerHyperparameter("param", 0, 10, log=True) + + with pytest.raises( + ValueError, + match="Upper bound 1 must be larger than lower bound 0 for " "hyperparameter param", + ): + _ = UniformIntegerHyperparameter( "param", 1, 0) + + +def test_uniformint_pdf(): + c1 = UniformIntegerHyperparameter("param", lower=0, upper=4) + c2 = UniformIntegerHyperparameter("logparam", lower=1, upper=10000, log=True) + c3 = UniformIntegerHyperparameter("param", lower=-1, upper=12) + point_1 = np.array([0]) + point_1_log = np.array([1]) + point_2 = np.array([3.0]) + point_2_log = np.array([3.0]) + non_integer_point = np.array([3.7]) + array_1 = np.array([1, 3, 3.7]) + point_outside_range = np.array([-1]) + point_outside_range_log = np.array([10001]) + wrong_shape_1 = np.array([[3]]) + wrong_shape_2 = np.array([3, 5, 7]).reshape(1, -1) + wrong_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) + + # need to lower the amount of places since the bounds + # are inexact (._lower=-0.49999, ._upper=4.49999) + assert c1.pdf(point_1)[0] == pytest.approx(0.2, abs=1e-5) + assert c2.pdf(point_1_log)[0] == pytest.approx(0.0001, abs=1e-5) + assert c1.pdf(point_2)[0] == pytest.approx(0.2, abs=1e-5) + assert c2.pdf(point_2_log)[0] == pytest.approx(0.0001, abs=1e-5) + assert c1.pdf(non_integer_point)[0] == pytest.approx(0.0, abs=1e-5) + assert c2.pdf(non_integer_point)[0] == pytest.approx(0.0, abs=1e-5) + assert c3.pdf(point_1)[0] == pytest.approx(0.07142857142857142, abs=1e-5) + + # TODO - change this once the is_legal support is there + # but does not have an actual impact of now + # since inverse_transform pulls everything into range, + # even points outside get evaluated in range + assert c1.pdf(point_outside_range)[0] == pytest.approx(0.2, abs=1e-5) + assert c2.pdf(point_outside_range_log)[0] == pytest.approx(0.0001, abs=1e-5) + + # this, however, is a negative value on a log param, which cannot be pulled into range + with pytest.warns(RuntimeWarning, match="invalid value encountered in log"): + assert c2.pdf(point_outside_range)[0] == 0.0 + + array_results = c1.pdf(array_1) + array_results_log = c2.pdf(array_1) + expected_results = np.array([0.2, 0.2, 0]) + expected_results_log = np.array([0.0001, 0.0001, 0]) + assert array_results.shape == pytest.approx(expected_results.shape) + assert array_results_log.shape == expected_results.shape + for res, log_res, exp_res, _ in zip( + array_results, + array_results, + expected_results, + expected_results_log, + ): + assert res == pytest.approx(exp_res, abs=1e-5) + assert log_res == pytest.approx(exp_res, abs=1e-5) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1.pdf(0.2) + with pytest.raises(TypeError): + c1.pdf("pdf") + + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_1) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_2) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_3) + + +def test_uniformint__pdf(): + c1 = UniformIntegerHyperparameter("param", lower=0, upper=4) + c2 = UniformIntegerHyperparameter("logparam", lower=1, upper=10000, log=True) + + point_1 = np.array([0]) + point_2 = np.array([0.7]) + array_1 = np.array([0, 0.7, 1.1]) + point_outside_range = np.array([-0.1]) + accepted_shape_1 = np.array([[0.7]]) + accepted_shape_2 = np.array([0, 0.7, 1.1]).reshape(1, -1) + accepted_shape_3 = np.array([1.1, 0.7, 0]).reshape(-1, 1) + + # need to lower the amount of places since the bounds + # are inexact (._lower=-0.49999, ._upper=4.49999) + assert c1._pdf(point_1)[0] == pytest.approx(0.2, abs=1e-5) + assert c2._pdf(point_1)[0] == pytest.approx(0.0001, abs=1e-5) + assert c1._pdf(point_2)[0] == pytest.approx(0.2, abs=1e-5) + assert c2._pdf(point_2)[0] == pytest.approx(0.0001, abs=1e-5) + + # TODO - change this once the is_legal support is there + # but does not have an actual impact of now + # since inverse_transform pulls everything into range, + # even points outside get evaluated in range + assert c1._pdf(point_outside_range)[0] == pytest.approx(0.0, abs=1e-5) + assert c2._pdf(point_outside_range)[0] == pytest.approx(0.0, abs=1e-5) + + array_results = c1._pdf(array_1) + array_results_log = c2._pdf(array_1) + expected_results = np.array([0.2, 0.2, 0]) + expected_results_log = np.array([0.0001, 0.0001, 0]) + assert array_results.shape == pytest.approx(expected_results.shape) + assert array_results_log.shape == expected_results.shape + for res, log_res, exp_res, exp_log_res in zip( + array_results, + array_results_log, + expected_results, + expected_results_log, + ): + assert res == pytest.approx(exp_res, abs=1e-5) + assert log_res == pytest.approx(exp_log_res, abs=1e-5) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1._pdf(0.2) + with pytest.raises(TypeError): + c1._pdf("pdf") + + # Simply check that it runs, since _pdf does not restrict shape (only public method does) + c1._pdf(accepted_shape_1) + c1._pdf(accepted_shape_2) + c1._pdf(accepted_shape_3) + + +def test_uniformint_get_max_density(): + c1 = UniformIntegerHyperparameter("param", lower=0, upper=4) + c2 = UniformIntegerHyperparameter("logparam", lower=1, upper=10000, log=True) + c3 = UniformIntegerHyperparameter("param", lower=-1, upper=12) + assert c1.get_max_density() == pytest.approx(0.2) + assert c2.get_max_density() == pytest.approx(0.0001) + assert c3.get_max_density() == pytest.approx(0.07142857142857142) + + +def test_uniformint_get_neighbors(): + rs = np.random.RandomState(seed=1) + for i_upper in range(1, 10): + c1 = UniformIntegerHyperparameter("param", lower=0, upper=i_upper) + for i_value in range(i_upper + 1): + float_value = c1._inverse_transform(np.array([i_value]))[0] + neighbors = c1.get_neighbors(float_value, rs, number=i_upper, transform=True) + assert set(neighbors) == set(range(i_upper + 1)) - {i_value} + + +def test_normalint(): + # TODO test for unequal! + f1 = NormalIntegerHyperparameter("param", 0.5, 5.5) + f1_ = NormalIntegerHyperparameter("param", 0.5, 5.5) + assert f1 == f1_ + assert str(f1) == "param, Type: NormalInteger, Mu: 0.5 Sigma: 5.5, Default: 0.5" + + # Test attributes are accessible + assert f1.name == "param" + assert f1.mu == 0.5 + assert f1.sigma == 5.5 + assert f1.q is None + assert f1.log is False + assert f1.default_value == pytest.approx(0.5) + assert f1.normalized_default_value == pytest.approx(0.5) + + with pytest.warns( + UserWarning, + match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", + ): + f2 = NormalIntegerHyperparameter("param", 0, 10, q=0.1) + with pytest.warns( + UserWarning, + match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", + ): + f2_ = NormalIntegerHyperparameter("param", 0, 10, q=0.1) + assert f2 == f2_ + assert str(f2) == "param, Type: NormalInteger, Mu: 0 Sigma: 10, Default: 0" + + f2_large_q = NormalIntegerHyperparameter("param", 0, 10, q=2) + f2_large_q_ = NormalIntegerHyperparameter("param", 0, 10, q=2) + assert f2_large_q == f2_large_q_ + assert str(f2_large_q) == "param, Type: NormalInteger, Mu: 0 Sigma: 10, Default: 0, Q: 2" + + f3 = NormalIntegerHyperparameter("param", 0, 10, log=True) + f3_ = NormalIntegerHyperparameter("param", 0, 10, log=True) + assert f3 == f3_ + assert str(f3) == "param, Type: NormalInteger, Mu: 0 Sigma: 10, Default: 1, on log-scale" + + f4 = NormalIntegerHyperparameter("param", 0, 10, default_value=3, log=True) + f4_ = NormalIntegerHyperparameter("param", 0, 10, default_value=3, log=True) + assert f4 == f4_ + assert str(f4) == "param, Type: NormalInteger, Mu: 0 Sigma: 10, Default: 3, on log-scale" + + with pytest.warns( + UserWarning, + match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", + ): + f5 = NormalIntegerHyperparameter("param", 0, 10, q=0.1, log=True) + f5_ = NormalIntegerHyperparameter("param", 0, 10, q=0.1, log=True) + assert f5 == f5_ + assert str(f5) == "param, Type: NormalInteger, Mu: 0 Sigma: 10, Default: 1, on log-scale" + + assert f1 != f2 + assert f1 != "UniformFloat" + + # test that meta-data is stored correctly + f_meta = NormalIntegerHyperparameter( + "param", + 0, + 10, + default_value=1, + log=True, + meta=dict(META_DATA), + ) + assert f_meta.meta == META_DATA + + # Test get_size + for int_hp in (f1, f2, f3, f4, f5): + assert np.isinf(int_hp.get_size()) + + # Unbounded case + f1 = NormalIntegerHyperparameter("param", 0, 10, q=1) + assert f1.get_neighbors(2, np.random.RandomState(9001), number=1) == [1] + assert f1.get_neighbors(2, np.random.RandomState(9001), number=5) == [0, 1, 9, 16, -1] + + # Bounded case + f1 = NormalIntegerHyperparameter("param", 0, 10, q=1, lower=-100, upper=100) + assert f1.get_neighbors(2, np.random.RandomState(9001), number=1) == [-11] + assert f1.get_neighbors(2, np.random.RandomState(9001), number=5) == [4, 11, 12, 15, -11] + + # Bounded case with default value out of bounds + with pytest.raises(ValueError): + _ = NormalIntegerHyperparameter("param", 5, 10, lower=1, upper=10, default_value=11) + + with pytest.raises(ValueError): + _ = NormalIntegerHyperparameter("param", 5, 10, lower=1, upper=10, default_value=0) + + +def test_normalint_legal_float_values(): + n_iter = NormalIntegerHyperparameter("n_iter", 0, 1.0, default_value=2.0) + assert isinstance(n_iter.default_value, int) + with pytest.raises( + ValueError, + match=r"For the Integer parameter n_iter, " + r"the value must be an Integer, too." + r" Right now it is a " + r"<(type|class) 'float'>" + r" with value 0.5.", + ): + _ = UniformIntegerHyperparameter("n_iter", 0, 1.0, default_value=0.5) + + +def test_normalint_to_uniform(): + with pytest.warns( + UserWarning, + match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", + ): + f1 = NormalIntegerHyperparameter("param", 0, 10, q=0.1) + f1_expected = UniformIntegerHyperparameter("param", -30, 30) + f1_actual = f1.to_uniform() + assert f1_expected == f1_actual + + +def test_normalint_is_legal(): + with pytest.warns( + UserWarning, + match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", + ): + f1 = NormalIntegerHyperparameter("param", 0, 10, q=0.1, log=True) + assert not f1.is_legal(3.1) + assert not f1.is_legal(3.0) # 3.0 behaves like an Integer + assert not f1.is_legal("BlaBlaBla") + assert f1.is_legal(2) + assert f1.is_legal(-15) + + # Test is legal vector + assert f1.is_legal_vector(1.0) + assert f1.is_legal_vector(0.0) + assert f1.is_legal_vector(0) + assert f1.is_legal_vector(0.3) + assert f1.is_legal_vector(-0.1) + assert f1.is_legal_vector(1.1) + with pytest.raises(TypeError): + f1.is_legal_vector("Hahaha") + + f2 = NormalIntegerHyperparameter("param", 5, 10, lower=1, upper=10, default_value=5) + assert f2.is_legal(5) + assert not f2.is_legal(0) + assert not f2.is_legal(11) + + +def test_normalint_pdf(): + c1 = NormalIntegerHyperparameter("param", lower=0, upper=10, mu=3, sigma=2) + c2 = NormalIntegerHyperparameter("logparam", lower=1, upper=1000, mu=3, sigma=2, log=True) + c3 = NormalIntegerHyperparameter("param", lower=0, upper=2, mu=-1.2, sigma=0.5) + + point_1 = np.array([3]) + point_1_log = np.array([10]) + point_2 = np.array([10]) + point_2_log = np.array([1000]) + point_3 = np.array([0]) + array_1 = np.array([3, 10, 11]) + array_1_log = np.array([10, 570, 1001]) + point_outside_range_1 = np.array([-1]) + point_outside_range_2 = np.array([11]) + point_outside_range_1_log = np.array([0]) + point_outside_range_2_log = np.array([1001]) + non_integer_point = np.array([5.7]) + wrong_shape_1 = np.array([[3]]) + wrong_shape_2 = np.array([3, 5, 7]).reshape(1, -1) + wrong_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) + + assert c1.pdf(point_1)[0] == pytest.approx(0.20747194595587332) + assert c2.pdf(point_1_log)[0] == pytest.approx(0.002625781612612434) + assert c1.pdf(point_2)[0] == pytest.approx(0.00045384303905059246) + assert c2.pdf(point_2_log)[0] == pytest.approx(0.0004136885586376241) + assert c3.pdf(point_3)[0] == pytest.approx(0.9988874412972069) + # TODO - change this once the is_legal support is there + # but does not have an actual impact of now + assert c1.pdf(point_outside_range_1)[0] == 0.0 + assert c1.pdf(point_outside_range_2)[0] == 0.0 + with pytest.warns(RuntimeWarning, match="divide by zero encountered in log"): + assert c2.pdf(point_outside_range_1_log)[0] == 0.0 + assert c2.pdf(point_outside_range_2_log)[0] == 0.0 + + assert c1.pdf(non_integer_point)[0] == 0.0 + assert c2.pdf(non_integer_point)[0] == 0.0 + + array_results = c1.pdf(array_1) + array_results_log = c2.pdf(array_1_log) + expected_results = np.array([0.20747194595587332, 0.00045384303905059246, 0]) + expected_results_log = np.array([0.002625781612612434, 0.000688676747843256, 0]) + assert array_results.shape == expected_results.shape + assert array_results_log.shape == expected_results.shape + for res, log_res, exp_res, exp_log_res in zip( + array_results, + array_results_log, + expected_results, + expected_results_log, + ): + assert res == pytest.approx(exp_res) + assert log_res == pytest.approx(exp_log_res) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1.pdf(0.2) + with pytest.raises(TypeError): + c1.pdf("pdf") + + c_nobounds = NormalFloatHyperparameter("param", mu=3, sigma=2) + assert c_nobounds.pdf(np.array([2]))[0] == pytest.approx(0.17603266338214976) + + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_1) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_2) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_3) + + +def test_normalint__pdf(): + c1 = NormalIntegerHyperparameter("param", lower=0, upper=10, mu=3, sigma=2) + c2 = NormalIntegerHyperparameter("logparam", lower=1, upper=1000, mu=3, sigma=2, log=True) + + point_1 = np.array([3]) + point_2 = np.array([5.2]) + array_1 = np.array([3, 5.2, 11]) + point_outside_range_1 = np.array([-1]) + point_outside_range_2 = np.array([11]) + + assert c1._pdf(point_1)[0] == pytest.approx(0.20747194595587332) + assert c2._pdf(point_1)[0] == pytest.approx(0.0027903779510164133) + assert c1._pdf(point_2)[0] == pytest.approx(0.1132951239316783) + assert c2._pdf(point_2)[0] == pytest.approx(0.001523754039709375) + # TODO - change this once the is_legal support is there + # but does not have an actual impact of now + assert c1._pdf(point_outside_range_1)[0] == 0.0 + assert c1._pdf(point_outside_range_2)[0] == 0.0 + assert c2._pdf(point_outside_range_1)[0] == 0.0 + assert c2._pdf(point_outside_range_2)[0] == 0.0 + + array_results = c1._pdf(array_1) + array_results_log = c2._pdf(array_1) + expected_results = np.array([0.20747194595587332, 0.1132951239316783, 0]) + expected_results_log = np.array([0.0027903779510164133, 0.001523754039709375, 0]) + assert array_results.shape == expected_results.shape + assert array_results_log.shape == expected_results.shape + for res, log_res, exp_res, exp_log_res in zip( + array_results, + array_results_log, + expected_results, + expected_results_log, + ): + assert res == pytest.approx(exp_res) + assert log_res == pytest.approx(exp_log_res) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1.pdf(0.2) + with pytest.raises(TypeError): + c1.pdf("pdf") + + c_nobounds = NormalFloatHyperparameter("param", mu=3, sigma=2) + assert c_nobounds.pdf(np.array([2]))[0] == pytest.approx(0.17603266338214976) + + +def test_normalint_get_max_density(): + c1 = NormalIntegerHyperparameter("param", lower=0, upper=10, mu=3, sigma=2) + c2 = NormalIntegerHyperparameter("logparam", lower=1, upper=1000, mu=3, sigma=2, log=True) + c3 = NormalIntegerHyperparameter("param", lower=0, upper=2, mu=-1.2, sigma=0.5) + assert c1.get_max_density() == pytest.approx(0.20747194595587332) + assert c2.get_max_density() == pytest.approx(0.002790371598208875) + assert c3.get_max_density() == pytest.approx(0.9988874412972069) + + +def test_normalint_compute_normalization(): + ARANGE_CHUNKSIZE = 10_000_000 + lower, upper = 1, ARANGE_CHUNKSIZE * 2 + + c = NormalIntegerHyperparameter("c", mu=10, sigma=500, lower=lower, upper=upper) + chunks = arange_chunked(lower, upper, chunk_size=ARANGE_CHUNKSIZE) + # exact computation over the complete range + N = sum(c.nfhp.pdf(chunk).sum() for chunk in chunks) + assert c.normalization_constant == pytest.approx(N, abs=1e-5) + + +############################################################ +def test_betaint(): + # TODO test non-equality + f1 = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.1) + f1_ = BetaIntegerHyperparameter("param", lower=-2, upper=2, alpha=3, beta=1.1) + assert f1 == f1_ + assert str(f1) == "param, Type: BetaInteger, Alpha: 3.0 Beta: 1.1, Range: [-2, 2], Default: 2" + + assert f1.alpha == pytest.approx(3.0) + assert f1.beta == pytest.approx(1.1) + + # Test copy + copy_f1 = copy.copy(f1) + assert copy_f1.name == f1.name + assert copy_f1.alpha == f1.alpha + assert copy_f1.beta == f1.beta + assert copy_f1.default_value == f1.default_value + + f2 = BetaIntegerHyperparameter("param", lower=-2.0, upper=4.0, alpha=3.0, beta=1.1, q=2) + f2_ = BetaIntegerHyperparameter("param", lower=-2, upper=4, alpha=3, beta=1.1, q=2) + assert f2 == f2_ + + assert ( + str(f2) + == "param, Type: BetaInteger, Alpha: 3.0 Beta: 1.1, Range: [-2, 4], Default: 4, Q: 2" + ) + + f3 = BetaIntegerHyperparameter("param", lower=1, upper=1000, alpha=3.0, beta=2.0, log=True) + f3_ = BetaIntegerHyperparameter("param", lower=1, upper=1000, alpha=3.0, beta=2.0, log=True) + assert f3 == f3_ + assert ( + str(f3) + == "param, Type: BetaInteger, Alpha: 3.0 Beta: 2.0, Range: [1, 1000], Default: 100, on log-scale" + ) + + with pytest.raises(ValueError): + BetaIntegerHyperparameter("param", lower=-1, upper=10.0, alpha=6.0, beta=2.0, log=True) + + # test that meta-data is stored correctly + f_meta = BetaFloatHyperparameter( + "param", + lower=1, + upper=10.0, + alpha=3.0, + beta=2.0, + log=False, + meta=dict(META_DATA), + ) + assert f_meta.meta == META_DATA + + +def test_betaint_default_value(): + # should default to the maximal value in the search space + f_max = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.0) + assert f_max.default_value == pytest.approx(2.0) + # since integer values are staggered over the normalized space + assert f_max.normalized_default_value == pytest.approx(0.9, abs=1e-4) + + # The normalized log defaults should be the same as if one were to create a uniform + # distribution with the same default value as is generated by the beta + f_max_log = BetaIntegerHyperparameter( + "param", + lower=1.0, + upper=10.0, + alpha=3.0, + beta=1.0, + log=True, + ) + assert f_max_log.default_value == pytest.approx(10.0) + assert f_max_log.normalized_default_value == pytest.approx(0.983974646746037) + + # should default to the minimal value in the search space + f_min = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=1.0, beta=1.5) + assert f_min.default_value == pytest.approx(-2.0) + assert f_min.normalized_default_value == pytest.approx(0.1, abs=1e-4) + + f_min_log = BetaIntegerHyperparameter( + "param", + lower=1.0, + upper=10.0, + alpha=1.0, + beta=1.5, + log=True, + ) + assert f_min_log.default_value == pytest.approx(1.0) + assert f_min_log.normalized_default_value == pytest.approx(0.22766524636349278) + + # Symmeric, should default to the middle + f_symm = BetaIntegerHyperparameter("param", lower=5, upper=9, alpha=4.6, beta=4.6) + assert f_symm.default_value == pytest.approx(7) + assert f_symm.normalized_default_value == pytest.approx(0.5) + + # This should yield a value that's approximately halfway towards the max in logspace + f_symm_log = BetaIntegerHyperparameter( + "param", + lower=1, + upper=np.round(np.exp(10)), + alpha=4.6, + beta=4.6, + log=True, + ) + assert f_symm_log.default_value == pytest.approx(148) + assert f_symm_log.normalized_default_value == pytest.approx(0.5321491582577761) + + # Uniform, should also default to the middle + f_unif = BetaIntegerHyperparameter("param", lower=2, upper=6, alpha=1.0, beta=1.0) + assert f_unif.default_value == pytest.approx(4) + assert f_unif.normalized_default_value == pytest.approx(0.5) + + # This should yield a value that's halfway towards the max in logspace + f_unif_log = BetaIntegerHyperparameter( + "param", + lower=1, + upper=np.round(np.exp(10)), + alpha=1, + beta=1, + log=True, + ) + assert f_unif_log.default_value == pytest.approx(148) + assert f_unif_log.normalized_default_value == pytest.approx(0.5321491582577761) + + # Then, test a case where the default value is the mode of the beta dist somewhere in + # the interior of the search space - but not the center + f_max = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=4.7, beta=2.12) + assert f_max.default_value == pytest.approx(1.0) + assert f_max.normalized_default_value == pytest.approx(0.7, abs=1e-4) + + f_max_log = BetaIntegerHyperparameter( + "param", + lower=1, + upper=np.round(np.exp(10)), + alpha=4.7, + beta=2.12, + log=True, + ) + assert f_max_log.default_value == pytest.approx(2157) + assert f_max_log.normalized_default_value == pytest.approx(0.7827083200774537) + + # These parameters yield a mode at approximately 1.1, so should thus yield default at 2 + f_quant = BetaIntegerHyperparameter( + "param", + lower=-2.0, + upper=2.0, + alpha=4.7, + beta=2.12, + q=2, + ) + assert f_quant.default_value == pytest.approx(2.0) + + # since it's quantized, it gets distributed evenly among the search space + # as such, the possible normalized defaults are 0.1, 0.3, 0.5, 0.7, 0.9 + assert f_quant.normalized_default_value == pytest.approx(0.9, abs=1e-4) + + # TODO log and quantization together does not yield a correct default for the beta + # hyperparameter, but it is relatively close to being correct. + + # The default value is independent of whether you log the parameter or not + f_legal_nolog = BetaIntegerHyperparameter( + "param", + lower=1, + upper=10.0, + alpha=3.0, + beta=2.0, + default_value=1, + log=True, + ) + f_legal_log = BetaIntegerHyperparameter( + "param", + lower=1, + upper=10.0, + alpha=3.0, + beta=2.0, + default_value=1, + log=False, + ) + + assert f_legal_nolog.default_value == pytest.approx(1) + assert f_legal_log.default_value == pytest.approx(1) + + # These are necessary, as we bypass the same check in the UniformFloatHP by design + with pytest.raises(ValueError, match="Illegal default value 0"): + BetaFloatHyperparameter( "param", lower=1, upper=10.0, alpha=3.0, beta=2.0, + default_value=0, log=False, - meta=dict(self.meta_data), ) - assert f_meta.meta == self.meta_data - - def test_betaint_default_value(self): - # should default to the maximal value in the search space - f_max = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.0) - self.assertAlmostEqual(f_max.default_value, 2.0) - # since integer values are staggered over the normalized space - self.assertAlmostEqual(f_max.normalized_default_value, 0.9, places=4) - - # The normalized log defaults should be the same as if one were to create a uniform - # distribution with the same default value as is generated by the beta - f_max_log = BetaIntegerHyperparameter( + with pytest.raises(ValueError, match="Illegal default value 0"): + BetaFloatHyperparameter( "param", - lower=1.0, - upper=10.0, + lower=1, + upper=1000.0, alpha=3.0, - beta=1.0, + beta=2.0, + default_value=0, log=True, ) - self.assertAlmostEqual(f_max_log.default_value, 10.0) - self.assertAlmostEqual(f_max_log.normalized_default_value, 0.983974646746037) - # should default to the minimal value in the search space - f_min = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=1.0, beta=1.5) - self.assertAlmostEqual(f_min.default_value, -2.0) - self.assertAlmostEqual(f_min.normalized_default_value, 0.1, places=4) - f_min_log = BetaIntegerHyperparameter( - "param", - lower=1.0, - upper=10.0, - alpha=1.0, - beta=1.5, - log=True, - ) - self.assertAlmostEqual(f_min_log.default_value, 1.0) - self.assertAlmostEqual(f_min_log.normalized_default_value, 0.22766524636349278) +def test_betaint_dist_parameters(): + # This one should just be created without raising an error - corresponds to uniform dist. + BetaIntegerHyperparameter("param", lower=0, upper=10.0, alpha=1, beta=1) + + # This one is not permitted as the co-domain is not finite + with pytest.raises(ValueError): + BetaIntegerHyperparameter("param", lower=0, upper=100, alpha=0.99, beta=0.99) + # And these parameters do not define a proper beta distribution whatsoever + with pytest.raises(ValueError): + BetaIntegerHyperparameter("param", lower=0, upper=100, alpha=-0.1, beta=-0.1) + + # test parameters that do not create a legit beta distribution, one at a time + with pytest.raises(ValueError): + BetaIntegerHyperparameter("param", lower=-2, upper=2, alpha=-11, beta=5) + with pytest.raises(ValueError): + BetaIntegerHyperparameter("param", lower=-2, upper=2, alpha=5, beta=-11) + + # test parameters that do not yield a finite co-domain, one at a time + with pytest.raises(ValueError): + BetaIntegerHyperparameter("param", lower=-2, upper=2, alpha=0.5, beta=11) + with pytest.raises(ValueError): + BetaIntegerHyperparameter("param", lower=-2, upper=2, alpha=11, beta=0.5) + + +def test_betaint_legal_float_values(): + f1 = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.1) + assert isinstance(f1.default_value, int) + with pytest.raises( + ValueError, + match="Illegal default value 0.5", + ): + _ = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.1, default_value=0.5) + + +def test_betaint_to_uniform(): + with pytest.warns( + UserWarning, + match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", + ): + f1 = BetaIntegerHyperparameter("param", lower=-30, upper=30, alpha=6.0, beta=2, q=0.1) + + f1_expected = UniformIntegerHyperparameter("param", -30, 30, default_value=20) + f1_actual = f1.to_uniform() + assert f1_expected == f1_actual + + +def test_betaint_pdf(): + c1 = BetaIntegerHyperparameter("param", alpha=3, beta=2, lower=0, upper=10) + c2 = BetaIntegerHyperparameter("logparam", alpha=3, beta=2, lower=1, upper=1000, log=True) + c3 = BetaIntegerHyperparameter("param", alpha=1.1, beta=10, lower=0, upper=3) + + point_1 = np.array([3]) + point_1_log = np.array([9]) + point_2 = np.array([9]) + point_2_log = np.array([570]) + point_3 = np.array([1]) + array_1 = np.array([3, 9, 11]) + array_1_log = np.array([9, 570, 1001]) + point_outside_range_1 = np.array([-1]) + point_outside_range_2 = np.array([11]) + point_outside_range_1_log = np.array([0]) + point_outside_range_2_log = np.array([1001]) + non_integer_point = np.array([5.7]) + wrong_shape_1 = np.array([[3]]) + wrong_shape_2 = np.array([3, 5, 7]).reshape(1, -1) + wrong_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) + + # The quantization constant (0.4999) dictates the accuracy of the integer beta pdf + assert c1.pdf(point_1)[0] == pytest.approx(0.07636363636363634, abs=1e-3) + assert c2.pdf(point_1_log)[0] == pytest.approx(0.0008724511426701984, abs=1e-3) + assert c1.pdf(point_2)[0] == pytest.approx(0.09818181818181816, abs=1e-3) + assert c2.pdf(point_2_log)[0] == pytest.approx(0.0008683622684160343, abs=1e-3) + assert c3.pdf(point_3)[0] == pytest.approx(0.9979110652388783, abs=1e-3) + + assert c1.pdf(point_outside_range_1)[0] == 0.0 + assert c1.pdf(point_outside_range_2)[0] == 0.0 + with pytest.warns(RuntimeWarning, match="divide by zero encountered in log"): + assert c2.pdf(point_outside_range_1_log)[0] == 0.0 + assert c2.pdf(point_outside_range_2_log)[0] == 0.0 + + assert c1.pdf(non_integer_point)[0] == 0.0 + assert c2.pdf(non_integer_point)[0] == 0.0 + + array_results = c1.pdf(array_1) + array_results_log = c2.pdf(array_1_log) + expected_results = np.array([0.07636363636363634, 0.09818181818181816, 0]) + expected_results_log = np.array([0.0008724511426701984, 0.0008683622684160343, 0]) + assert array_results.shape == expected_results.shape + assert array_results_log.shape == expected_results.shape + for res, log_res, exp_res, exp_log_res in zip( + array_results, + array_results_log, + expected_results, + expected_results_log, + ): + assert res == pytest.approx(exp_res, abs=1e-3) + assert log_res == pytest.approx(exp_log_res, abs=1e-3) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1.pdf(0.2) + with pytest.raises(TypeError): + c1.pdf("pdf") + + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_1) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_2) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_3) + + +def test_betaint__pdf(): + c1 = BetaIntegerHyperparameter("param", alpha=3, beta=2, lower=0, upper=10) + c2 = BetaIntegerHyperparameter( + "logparam", + alpha=3, + beta=2, + lower=1, + upper=np.round(np.exp(10)), + log=True, + ) + + # since the logged and unlogged parameters will have different active domains + # in the unit range, they will not evaluate identically under _pdf + point_1 = np.array([0.249995]) + point_1_log = np.array([0.345363]) + point_2 = np.array([0.850001]) + point_2_log = np.array([0.906480]) + array_1 = np.array([0.249995, 0.850001, 0.045]) + array_1_log = np.array([0.345363, 0.906480, 0.065]) + point_outside_range_1 = np.array([0.045]) + point_outside_range_1_log = np.array([0.06]) + point_outside_range_2 = np.array([0.96]) + + accepted_shape_1 = np.array([[3]]) + accepted_shape_2 = np.array([3, 5, 7]).reshape(1, -1) + accepted_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) + + assert c1._pdf(point_1)[0] == pytest.approx(0.0475566) + assert c2._pdf(point_1_log)[0] == pytest.approx(0.00004333811) + assert c1._pdf(point_2)[0] == pytest.approx(0.1091810) + assert c2._pdf(point_2_log)[0] == pytest.approx(0.00005571951) + + # test points that are actually outside of the _pdf range due to the skewing + # of the unit hypercube space + assert c1._pdf(point_outside_range_1)[0] == 0.0 + assert c1._pdf(point_outside_range_2)[0] == 0.0 + assert c2._pdf(point_outside_range_1_log)[0] == 0.0 + + array_results = c1._pdf(array_1) + array_results_log = c2._pdf(array_1_log) + expected_results = np.array([0.0475566, 0.1091810, 0]) + expected_results_log = np.array([0.00004333811, 0.00005571951, 0]) + assert array_results.shape == expected_results.shape + assert array_results_log.shape == expected_results_log.shape + for res, log_res, exp_res, exp_log_res in zip( + array_results, + array_results_log, + expected_results, + expected_results_log, + ): + assert res == pytest.approx(exp_res) + assert log_res == pytest.approx(exp_log_res) + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1._pdf(0.2) + with pytest.raises(TypeError): + c1._pdf("pdf") + + # Simply check that it runs, since _pdf does not restrict shape (only public method does) + c1._pdf(accepted_shape_1) + c1._pdf(accepted_shape_2) + c1._pdf(accepted_shape_3) + + +def test_betaint_get_max_density(): + c1 = BetaIntegerHyperparameter("param", alpha=3, beta=2, lower=0, upper=10) + c2 = BetaIntegerHyperparameter("logparam", alpha=3, beta=2, lower=1, upper=1000, log=True) + c3 = BetaIntegerHyperparameter("param", alpha=1.1, beta=10, lower=0, upper=3) + assert c1.get_max_density() == pytest.approx(0.1781818181818181) + assert c2.get_max_density() == pytest.approx(0.0018733953504422762) + assert c3.get_max_density() == pytest.approx(0.9979110652388783) + + +def test_betaint_compute_normalization(): + ARANGE_CHUNKSIZE = 10_000_000 + lower, upper = 0, ARANGE_CHUNKSIZE * 2 + + c = BetaIntegerHyperparameter("c", alpha=3, beta=2, lower=lower, upper=upper) + chunks = arange_chunked(lower, upper, chunk_size=ARANGE_CHUNKSIZE) + # exact computation over the complete range + N = sum(c.bfhp.pdf(chunk).sum() for chunk in chunks) + assert c.normalization_constant == pytest.approx(N, abs=1e-5) + + +def test_categorical(): + # TODO test for inequality + f1 = CategoricalHyperparameter("param", [0, 1]) + f1_ = CategoricalHyperparameter("param", [0, 1]) + assert f1 == f1_ + assert str(f1) == "param, Type: Categorical, Choices: {0, 1}, Default: 0" + + # Test attributes are accessible + assert f1.name == "param" + assert f1.num_choices == 2 + assert f1.default_value == 0 + assert f1.normalized_default_value == 0 + assert f1.probabilities == pytest.approx((0.5, 0.5)) + + f2 = CategoricalHyperparameter("param", list(range(1000))) + f2_ = CategoricalHyperparameter("param", list(range(1000))) + assert f2 == f2_ + assert "param, Type: Categorical, Choices: {%s}, Default: 0" % ", ".join( + [str(choice) for choice in range(1000)], + ) == str(f2) + + f3 = CategoricalHyperparameter("param", list(range(999))) + assert f2 != f3 + + f4 = CategoricalHyperparameter("param_", list(range(1000))) + assert f2 != f4 + + f5 = CategoricalHyperparameter("param", [*list(range(999)), 1001]) + assert f2 != f5 + + f6 = CategoricalHyperparameter("param", ["a", "b"], default_value="b") + f6_ = CategoricalHyperparameter("param", ["a", "b"], default_value="b") + assert f6 == f6_ + assert str(f6) == "param, Type: Categorical, Choices: {a, b}, Default: b" + + assert f1 != f2 + assert f1 != "UniformFloat" + + # test that meta-data is stored correctly + f_meta = CategoricalHyperparameter( + "param", + ["a", "b"], + default_value="a", + meta=dict(META_DATA), + ) + assert f_meta.meta == META_DATA + + assert f1.get_size() == 2 + assert f2.get_size() == 1000 + assert f3.get_size() == 999 + assert f4.get_size() == 1000 + assert f5.get_size() == 1000 + assert f6.get_size() == 2 + + +def test_cat_equal(): + # Test that weights are properly normalized and compared + c1 = CategoricalHyperparameter("param", ["a", "b"], weights=[2, 2]) + other = CategoricalHyperparameter("param", ["a", "b"]) + assert c1 == other + + c1 = CategoricalHyperparameter("param", ["a", "b"]) + other = CategoricalHyperparameter("param", ["a", "b"], weights=[2, 2]) + assert c1 == other + + c1 = CategoricalHyperparameter("param", ["a", "b"], weights=[1, 2]) + other = CategoricalHyperparameter("param", ["a", "b"], weights=[10, 20]) + assert c1 == other + + # These result in different default values and are therefore different + c1 = CategoricalHyperparameter("param", ["a", "b"]) + c2 = CategoricalHyperparameter("param", ["b", "a"]) + assert c1 != c2 + + # Test that the order of the hyperparameter doesn't matter if the default is given + c1 = CategoricalHyperparameter("param", ["a", "b"], default_value="a") + c2 = CategoricalHyperparameter("param", ["b", "a"], default_value="a") + assert c1 == c2 + + # Test that the weights are ordered correctly + c1 = CategoricalHyperparameter("param", ["a", "b"], weights=[1, 2], default_value="a") + c2 = CategoricalHyperparameter("param", ["b", "a"], weights=[2, 1], default_value="a") + assert c1 == c2 + + c1 = CategoricalHyperparameter("param", ["a", "b"], weights=[1, 2], default_value="a") + c2 = CategoricalHyperparameter("param", ["b", "a"], weights=[1, 2], default_value="a") + assert c1 != c2 + + c1 = CategoricalHyperparameter("param", ["a", "b"], weights=[1, 2], default_value="a") + c2 = CategoricalHyperparameter("param", ["b", "a"], default_value="a") + assert c1 != c2 + + c1 = CategoricalHyperparameter("param", ["a", "b"], default_value="a") + c2 = CategoricalHyperparameter("param", ["b", "a"], weights=[1, 2], default_value="a") + assert c1 != c2 + + # Test that the equals operator does not fail accessing the weight of choice "a" in c2 + c1 = CategoricalHyperparameter("param", ["a", "b"], weights=[1, 2]) + c2 = CategoricalHyperparameter("param", ["b", "c"], weights=[1, 2]) + assert c1 != c2 + + +def test_categorical_strings(): + f1 = CategoricalHyperparameter("param", ["a", "b"]) + f1_ = CategoricalHyperparameter("param", ["a", "b"]) + assert f1 == f1_ + assert str(f1) == "param, Type: Categorical, Choices: {a, b}, Default: a" + + +def test_categorical_is_legal(): + f1 = CategoricalHyperparameter("param", ["a", "b"]) + assert f1.is_legal("a") + assert f1.is_legal("a") + assert not f1.is_legal("c") + assert not f1.is_legal(3) + + # Test is legal vector + assert f1.is_legal_vector(1.0) + assert f1.is_legal_vector(0.0) + assert f1.is_legal_vector(0) + assert not f1.is_legal_vector(0.3) + assert not f1.is_legal_vector(-0.1) + with pytest.raises(TypeError): + f1.is_legal_vector("Hahaha") + + +def test_categorical_choices(): + with pytest.raises( + ValueError, + match="Choices for categorical hyperparameters param contain choice 'a' 2 times, " + "while only a single oocurence is allowed.", + ): + CategoricalHyperparameter("param", ["a", "a"]) + + with pytest.raises(TypeError, match="Choice 'None' is not supported"): + CategoricalHyperparameter("param", ["a", None]) + + +def test_categorical_default(): + # Test that the default value is the most probable choice when weights are given + f1 = CategoricalHyperparameter("param", ["a", "b"]) + f2 = CategoricalHyperparameter("param", ["a", "b"], weights=[0.3, 0.6]) + f3 = CategoricalHyperparameter("param", ["a", "b"], weights=[0.6, 0.3]) + assert f1.default_value != f2.default_value + assert f1.default_value == f3.default_value + + +def test_sample_UniformFloatHyperparameter(): + # This can sample four distributions + def sample(hp): + rs = np.random.RandomState(1) + counts_per_bin = [0 for _ in range(21)] + value = None + for _ in range(100000): + value = hp.sample(rs) + if hp.log: + assert value <= np.exp(hp._upper) + assert value >= np.exp(hp._lower) + else: + assert value <= hp._upper + assert value >= hp._lower + index = int((value - hp.lower) / (hp.upper - hp.lower) * 20) + counts_per_bin[index] += 1 + + assert isinstance(value, float) + return counts_per_bin + + # Uniform + hp = UniformFloatHyperparameter("ufhp", 0.5, 2.5) + + counts_per_bin = sample(hp) + # The 21st bin is only filled if exactly 2.5 is sampled...very rare... + for bin in counts_per_bin[:-1]: + assert 5200 > bin > 4800 + assert sample(hp) == sample(hp) + + # Quantized Uniform + hp = UniformFloatHyperparameter("ufhp", 0.0, 1.0, q=0.1) + + counts_per_bin = sample(hp) + for bin in counts_per_bin[::2]: + assert 9301 > bin > 8700 + for bin in counts_per_bin[1::2]: + assert bin == 0 + assert sample(hp) == sample(hp) + + # Log Uniform + hp = UniformFloatHyperparameter("ufhp", 1.0, np.e**2, log=True) + + counts_per_bin = sample(hp) + assert counts_per_bin == [ + 14012, + 10977, + 8809, + 7559, + 6424, + 5706, + 5276, + 4694, + 4328, + 3928, + 3655, + 3386, + 3253, + 2932, + 2816, + 2727, + 2530, + 2479, + 2280, + 2229, + 0, + ] + assert sample(hp) == sample(hp) + + # Quantized Log-Uniform + # 7.2 ~ np.round(e * e, 1) + hp = UniformFloatHyperparameter("ufhp", 1.2, 7.2, q=0.6, log=True) + + counts_per_bin = sample(hp) + assert counts_per_bin == [ + 24359, + 15781, + 0, + 11635, + 0, + 0, + 9506, + 7867, + 0, + 0, + 6763, + 0, + 5919, + 0, + 5114, + 4798, + 0, + 0, + 4339, + 0, + 3919, + ] + assert sample(hp) == sample(hp) + + # Issue #199 + hp = UniformFloatHyperparameter("uni_float_q", lower=1e-4, upper=1e-1, q=1e-5, log=True) + assert np.isfinite(hp._lower) + assert np.isfinite(hp._upper) + sample(hp) + + +def test_categorical_pdf(): + c1 = CategoricalHyperparameter("x1", choices=["one", "two", "three"], weights=[2, 1, 2]) + c2 = CategoricalHyperparameter("x1", choices=["one", "two", "three"], weights=[5, 0, 2]) + c3 = CategoricalHyperparameter("x1", choices=["one", "two", "three", "four"]) + + point_1 = np.array(["one"]) + point_2 = np.array(["two"]) + + wrong_shape_1 = np.array([["one"]]) + wrong_shape_2 = np.array(["one", "two"]).reshape(1, -1) + wrong_shape_3 = np.array(["one", "two"]).reshape(-1, 1) + + assert c1.pdf(point_1)[0] == 0.4 + assert c1.pdf(point_2)[0] == 0.2 + assert c2.pdf(point_1)[0] == pytest.approx(0.7142857142857143) + assert c2.pdf(point_2)[0] == 0.0 + assert c3.pdf(point_1)[0] == 0.25 + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1.pdf(0.2) + with pytest.raises(TypeError): + c1.pdf("pdf") + with pytest.raises(TypeError): + c1.pdf("one") + with pytest.raises(ValueError): + c1.pdf(np.array(["zero"])) + + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_1) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_2) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_3) + + +def test_categorical__pdf(): + c1 = CategoricalHyperparameter("x1", choices=["one", "two", "three"], weights=[2, 1, 2]) + c2 = CategoricalHyperparameter("x1", choices=["one", "two", "three"], weights=[5, 0, 2]) + + point_1 = np.array([0]) + point_2 = np.array([1]) + array_1 = np.array([1, 0, 2]) + nan = np.array([0, np.nan]) + assert c1._pdf(point_1)[0] == 0.4 + assert c1._pdf(point_2)[0] == 0.2 + assert c2._pdf(point_1)[0] == pytest.approx(0.7142857142857143) + assert c2._pdf(point_2)[0] == 0.0 + + array_results = c1._pdf(array_1) + expected_results = np.array([0.2, 0.4, 0.4]) + assert array_results.shape == expected_results.shape + for res, exp_res in zip(array_results, expected_results): + assert res == exp_res + + nan_results = c1._pdf(nan) + expected_results = np.array([0.4, 0]) + assert nan_results.shape == expected_results.shape + for res, exp_res in zip(nan_results, expected_results): + assert res == exp_res + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1._pdf(0.2) + with pytest.raises(TypeError): + c1._pdf("pdf") + with pytest.raises(TypeError): + c1._pdf("one") + with pytest.raises(TypeError): + c1._pdf(np.array(["zero"])) + + +def test_categorical_get_max_density(): + c1 = CategoricalHyperparameter("x1", choices=["one", "two", "three"], weights=[2, 1, 2]) + c2 = CategoricalHyperparameter("x1", choices=["one", "two", "three"], weights=[5, 0, 2]) + c3 = CategoricalHyperparameter("x1", choices=["one", "two", "three"]) + assert c1.get_max_density() == 0.4 + assert c2.get_max_density() == 0.7142857142857143 + assert c3.get_max_density() == pytest.approx(0.33333333333333) + + +def test_sample_NormalFloatHyperparameter(): + hp = NormalFloatHyperparameter("nfhp", 0, 1) + + def actual_test(): + rs = np.random.RandomState(1) + counts_per_bin = [0 for _ in range(11)] + value = None + for _ in range(100000): + value = hp.sample(rs) + index = min(max(int((np.round(value + 0.5)) + 5), 0), 9) + counts_per_bin[index] += 1 - # Symmeric, should default to the middle - f_symm = BetaIntegerHyperparameter("param", lower=5, upper=9, alpha=4.6, beta=4.6) - self.assertAlmostEqual(f_symm.default_value, 7) - self.assertAlmostEqual(f_symm.normalized_default_value, 0.5) + assert [0, 4, 138, 2113, 13394, 34104, 34282, 13683, 2136, 146, 0] == counts_per_bin - # This should yield a value that's approximately halfway towards the max in logspace - f_symm_log = BetaIntegerHyperparameter( - "param", - lower=1, - upper=np.round(np.exp(10)), - alpha=4.6, - beta=4.6, - log=True, - ) - self.assertAlmostEqual(f_symm_log.default_value, 148) - self.assertAlmostEqual(f_symm_log.normalized_default_value, 0.5321491582577761) + assert isinstance(value, float) + return counts_per_bin - # Uniform, should also default to the middle - f_unif = BetaIntegerHyperparameter("param", lower=2, upper=6, alpha=1.0, beta=1.0) - self.assertAlmostEqual(f_unif.default_value, 4) - self.assertAlmostEqual(f_unif.normalized_default_value, 0.5) + assert actual_test() == actual_test() - # This should yield a value that's halfway towards the max in logspace - f_unif_log = BetaIntegerHyperparameter( - "param", - lower=1, - upper=np.round(np.exp(10)), - alpha=1, - beta=1, - log=True, - ) - self.assertAlmostEqual(f_unif_log.default_value, 148) - self.assertAlmostEqual(f_unif_log.normalized_default_value, 0.5321491582577761) - # Then, test a case where the default value is the mode of the beta dist somewhere in - # the interior of the search space - but not the center - f_max = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=4.7, beta=2.12) - self.assertAlmostEqual(f_max.default_value, 1.0) - self.assertAlmostEqual(f_max.normalized_default_value, 0.7, places=4) +def test_sample_NormalFloatHyperparameter_with_bounds(): + hp = NormalFloatHyperparameter("nfhp", 0, 1, lower=-3, upper=3) - f_max_log = BetaIntegerHyperparameter( - "param", - lower=1, - upper=np.round(np.exp(10)), - alpha=4.7, - beta=2.12, - log=True, - ) - self.assertAlmostEqual(f_max_log.default_value, 2157) - self.assertAlmostEqual(f_max_log.normalized_default_value, 0.7827083200774537) + # TODO: This should probably be a smaller amount + def actual_test(): + rs = np.random.RandomState(1) + counts_per_bin = [0 for _ in range(11)] + value = None + for _ in range(100000): + value = hp.sample(rs) + index = min(max(int((np.round(value + 0.5)) + 5), 0), 9) + counts_per_bin[index] += 1 - # These parameters yield a mode at approximately 1.1, so should thus yield default at 2 - f_quant = BetaIntegerHyperparameter( - "param", - lower=-2.0, - upper=2.0, - alpha=4.7, - beta=2.12, - q=2, - ) - self.assertAlmostEqual(f_quant.default_value, 2.0) + assert [0, 0, 0, 2184, 13752, 34078, 34139, 13669, 2178, 0, 0] == counts_per_bin - # since it's quantized, it gets distributed evenly among the search space - # as such, the possible normalized defaults are 0.1, 0.3, 0.5, 0.7, 0.9 - self.assertAlmostEqual(f_quant.normalized_default_value, 0.9, places=4) + assert isinstance(value, float) + return counts_per_bin - # TODO log and quantization together does not yield a correct default for the beta - # hyperparameter, but it is relatively close to being correct. + assert actual_test() == actual_test() - # The default value is independent of whether you log the parameter or not - f_legal_nolog = BetaIntegerHyperparameter( - "param", - lower=1, - upper=10.0, - alpha=3.0, - beta=2.0, - default_value=1, - log=True, - ) - f_legal_log = BetaIntegerHyperparameter( - "param", - lower=1, - upper=10.0, - alpha=3.0, - beta=2.0, - default_value=1, - log=False, - ) - self.assertAlmostEqual(f_legal_nolog.default_value, 1) - self.assertAlmostEqual(f_legal_log.default_value, 1) - - # These are necessary, as we bypass the same check in the UniformFloatHP by design - with self.assertRaisesRegex(ValueError, "Illegal default value 0"): - BetaFloatHyperparameter( - "param", - lower=1, - upper=10.0, - alpha=3.0, - beta=2.0, - default_value=0, - log=False, - ) - with self.assertRaisesRegex(ValueError, "Illegal default value 0"): - BetaFloatHyperparameter( - "param", - lower=1, - upper=1000.0, - alpha=3.0, - beta=2.0, - default_value=0, - log=True, - ) - - def test_betaint_dist_parameters(self): - # This one should just be created without raising an error - corresponds to uniform dist. - BetaIntegerHyperparameter("param", lower=0, upper=10.0, alpha=1, beta=1) - - # This one is not permitted as the co-domain is not finite - with self.assertRaises(ValueError): - BetaIntegerHyperparameter("param", lower=0, upper=100, alpha=0.99, beta=0.99) - # And these parameters do not define a proper beta distribution whatsoever - with self.assertRaises(ValueError): - BetaIntegerHyperparameter("param", lower=0, upper=100, alpha=-0.1, beta=-0.1) - - # test parameters that do not create a legit beta distribution, one at a time - with self.assertRaises(ValueError): - BetaIntegerHyperparameter("param", lower=-2, upper=2, alpha=-11, beta=5) - with self.assertRaises(ValueError): - BetaIntegerHyperparameter("param", lower=-2, upper=2, alpha=5, beta=-11) - - # test parameters that do not yield a finite co-domain, one at a time - with self.assertRaises(ValueError): - BetaIntegerHyperparameter("param", lower=-2, upper=2, alpha=0.5, beta=11) - with self.assertRaises(ValueError): - BetaIntegerHyperparameter("param", lower=-2, upper=2, alpha=11, beta=0.5) - - def test_betaint_legal_float_values(self): - f1 = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.1) - assert isinstance(f1.default_value, int) - self.assertRaisesRegex( - ValueError, - "Illegal default value 0.5", - BetaIntegerHyperparameter, - "param", - lower=-2.0, - upper=2.0, - alpha=3.0, - beta=1.1, - default_value=0.5, - ) +def test_sample_BetaFloatHyperparameter(): + hp = BetaFloatHyperparameter("bfhp", alpha=8, beta=1.5, lower=-1, upper=10) - def test_betaint_to_uniform(self): - with pytest.warns( - UserWarning, - match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", - ): - f1 = BetaIntegerHyperparameter("param", lower=-30, upper=30, alpha=6.0, beta=2, q=0.1) - - f1_expected = UniformIntegerHyperparameter("param", -30, 30, default_value=20) - f1_actual = f1.to_uniform() - assert f1_expected == f1_actual - - def test_betaint_pdf(self): - c1 = BetaIntegerHyperparameter("param", alpha=3, beta=2, lower=0, upper=10) - c2 = BetaIntegerHyperparameter("logparam", alpha=3, beta=2, lower=1, upper=1000, log=True) - c3 = BetaIntegerHyperparameter("param", alpha=1.1, beta=10, lower=0, upper=3) - - point_1 = np.array([3]) - point_1_log = np.array([9]) - point_2 = np.array([9]) - point_2_log = np.array([570]) - point_3 = np.array([1]) - array_1 = np.array([3, 9, 11]) - array_1_log = np.array([9, 570, 1001]) - point_outside_range_1 = np.array([-1]) - point_outside_range_2 = np.array([11]) - point_outside_range_1_log = np.array([0]) - point_outside_range_2_log = np.array([1001]) - non_integer_point = np.array([5.7]) - wrong_shape_1 = np.array([[3]]) - wrong_shape_2 = np.array([3, 5, 7]).reshape(1, -1) - wrong_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) - - # The quantization constant (0.4999) dictates the accuracy of the integer beta pdf - self.assertAlmostEqual(c1.pdf(point_1)[0], 0.07636363636363634, places=3) - self.assertAlmostEqual(c2.pdf(point_1_log)[0], 0.0008724511426701984, places=3) - self.assertAlmostEqual(c1.pdf(point_2)[0], 0.09818181818181816, places=3) - self.assertAlmostEqual(c2.pdf(point_2_log)[0], 0.0008683622684160343, places=3) - self.assertAlmostEqual(c3.pdf(point_3)[0], 0.9979110652388783, places=3) - - assert c1.pdf(point_outside_range_1)[0] == 0.0 - assert c1.pdf(point_outside_range_2)[0] == 0.0 - with pytest.warns(RuntimeWarning, match="divide by zero encountered in log"): - assert c2.pdf(point_outside_range_1_log)[0] == 0.0 - assert c2.pdf(point_outside_range_2_log)[0] == 0.0 - - assert c1.pdf(non_integer_point)[0] == 0.0 - assert c2.pdf(non_integer_point)[0] == 0.0 - - array_results = c1.pdf(array_1) - array_results_log = c2.pdf(array_1_log) - expected_results = np.array([0.07636363636363634, 0.09818181818181816, 0]) - expected_results_log = np.array([0.0008724511426701984, 0.0008683622684160343, 0]) - assert array_results.shape == expected_results.shape - assert array_results_log.shape == expected_results.shape - for res, log_res, exp_res, exp_log_res in zip( - array_results, - array_results_log, - expected_results, - expected_results_log, - ): - self.assertAlmostEqual(res, exp_res, places=3) - self.assertAlmostEqual(log_res, exp_log_res, places=3) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1.pdf(0.2) - with self.assertRaises(TypeError): - c1.pdf("pdf") - - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_1) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_2) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_3) - - def test_betaint__pdf(self): - c1 = BetaIntegerHyperparameter("param", alpha=3, beta=2, lower=0, upper=10) - c2 = BetaIntegerHyperparameter( - "logparam", - alpha=3, - beta=2, - lower=1, - upper=np.round(np.exp(10)), - log=True, - ) - - # since the logged and unlogged parameters will have different active domains - # in the unit range, they will not evaluate identically under _pdf - point_1 = np.array([0.249995]) - point_1_log = np.array([0.345363]) - point_2 = np.array([0.850001]) - point_2_log = np.array([0.906480]) - array_1 = np.array([0.249995, 0.850001, 0.045]) - array_1_log = np.array([0.345363, 0.906480, 0.065]) - point_outside_range_1 = np.array([0.045]) - point_outside_range_1_log = np.array([0.06]) - point_outside_range_2 = np.array([0.96]) - - accepted_shape_1 = np.array([[3]]) - accepted_shape_2 = np.array([3, 5, 7]).reshape(1, -1) - accepted_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) - - self.assertAlmostEqual(c1._pdf(point_1)[0], 0.0475566) - self.assertAlmostEqual(c2._pdf(point_1_log)[0], 0.00004333811) - self.assertAlmostEqual(c1._pdf(point_2)[0], 0.1091810) - self.assertAlmostEqual(c2._pdf(point_2_log)[0], 0.00005571951) - - # test points that are actually outside of the _pdf range due to the skewing - # of the unit hypercube space - assert c1._pdf(point_outside_range_1)[0] == 0.0 - assert c1._pdf(point_outside_range_2)[0] == 0.0 - assert c2._pdf(point_outside_range_1_log)[0] == 0.0 - - array_results = c1._pdf(array_1) - array_results_log = c2._pdf(array_1_log) - expected_results = np.array([0.0475566, 0.1091810, 0]) - expected_results_log = np.array([0.00004333811, 0.00005571951, 0]) - assert array_results.shape == expected_results.shape - assert array_results_log.shape == expected_results_log.shape - for res, log_res, exp_res, exp_log_res in zip( - array_results, - array_results_log, - expected_results, - expected_results_log, - ): - self.assertAlmostEqual(res, exp_res) - self.assertAlmostEqual(log_res, exp_log_res) - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1._pdf(0.2) - with self.assertRaises(TypeError): - c1._pdf("pdf") - - # Simply check that it runs, since _pdf does not restrict shape (only public method does) - c1._pdf(accepted_shape_1) - c1._pdf(accepted_shape_2) - c1._pdf(accepted_shape_3) - - def test_betaint_get_max_density(self): - c1 = BetaIntegerHyperparameter("param", alpha=3, beta=2, lower=0, upper=10) - c2 = BetaIntegerHyperparameter("logparam", alpha=3, beta=2, lower=1, upper=1000, log=True) - c3 = BetaIntegerHyperparameter("param", alpha=1.1, beta=10, lower=0, upper=3) - self.assertAlmostEqual(c1.get_max_density(), 0.1781818181818181) - self.assertAlmostEqual(c2.get_max_density(), 0.0018733953504422762) - self.assertAlmostEqual(c3.get_max_density(), 0.9979110652388783) - - def test_betaint_compute_normalization(self): - ARANGE_CHUNKSIZE = 10_000_000 - lower, upper = 0, ARANGE_CHUNKSIZE * 2 - - c = BetaIntegerHyperparameter("c", alpha=3, beta=2, lower=lower, upper=upper) - chunks = arange_chunked(lower, upper, chunk_size=ARANGE_CHUNKSIZE) - # exact computation over the complete range - N = sum(c.bfhp.pdf(chunk).sum() for chunk in chunks) - self.assertAlmostEqual(c.normalization_constant, N, places=5) - - def test_categorical(self): - # TODO test for inequality - f1 = CategoricalHyperparameter("param", [0, 1]) - f1_ = CategoricalHyperparameter("param", [0, 1]) - assert f1 == f1_ - assert str(f1) == "param, Type: Categorical, Choices: {0, 1}, Default: 0" - - # Test attributes are accessible - assert f1.name == "param" - assert f1.num_choices == 2 - assert f1.default_value == 0 - assert f1.normalized_default_value == 0 - self.assertTupleEqual(f1.probabilities, (0.5, 0.5)) - - f2 = CategoricalHyperparameter("param", list(range(0, 1000))) - f2_ = CategoricalHyperparameter("param", list(range(0, 1000))) - assert f2 == f2_ - assert "param, Type: Categorical, Choices: {%s}, Default: 0" % ", ".join( - [str(choice) for choice in range(0, 1000)], - ) == str(f2) - - f3 = CategoricalHyperparameter("param", list(range(0, 999))) - assert f2 != f3 - - f4 = CategoricalHyperparameter("param_", list(range(0, 1000))) - assert f2 != f4 - - f5 = CategoricalHyperparameter("param", [*list(range(0, 999)), 1001]) - assert f2 != f5 - - f6 = CategoricalHyperparameter("param", ["a", "b"], default_value="b") - f6_ = CategoricalHyperparameter("param", ["a", "b"], default_value="b") - assert f6 == f6_ - assert str(f6) == "param, Type: Categorical, Choices: {a, b}, Default: b" - - assert f1 != f2 - assert f1 != "UniformFloat" - - # test that meta-data is stored correctly - f_meta = CategoricalHyperparameter( - "param", - ["a", "b"], - default_value="a", - meta=dict(self.meta_data), - ) - assert f_meta.meta == self.meta_data - - assert f1.get_size() == 2 - assert f2.get_size() == 1000 - assert f3.get_size() == 999 - assert f4.get_size() == 1000 - assert f5.get_size() == 1000 - assert f6.get_size() == 2 - - def test_cat_equal(self): - # Test that weights are properly normalized and compared - c1 = CategoricalHyperparameter("param", ["a", "b"], weights=[2, 2]) - other = CategoricalHyperparameter("param", ["a", "b"]) - assert c1 == other - - c1 = CategoricalHyperparameter("param", ["a", "b"]) - other = CategoricalHyperparameter("param", ["a", "b"], weights=[2, 2]) - assert c1 == other - - c1 = CategoricalHyperparameter("param", ["a", "b"], weights=[1, 2]) - other = CategoricalHyperparameter("param", ["a", "b"], weights=[10, 20]) - assert c1 == other - - # These result in different default values and are therefore different - c1 = CategoricalHyperparameter("param", ["a", "b"]) - c2 = CategoricalHyperparameter("param", ["b", "a"]) - assert c1 != c2 - - # Test that the order of the hyperparameter doesn't matter if the default is given - c1 = CategoricalHyperparameter("param", ["a", "b"], default_value="a") - c2 = CategoricalHyperparameter("param", ["b", "a"], default_value="a") - assert c1 == c2 - - # Test that the weights are ordered correctly - c1 = CategoricalHyperparameter("param", ["a", "b"], weights=[1, 2], default_value="a") - c2 = CategoricalHyperparameter("param", ["b", "a"], weights=[2, 1], default_value="a") - assert c1 == c2 - - c1 = CategoricalHyperparameter("param", ["a", "b"], weights=[1, 2], default_value="a") - c2 = CategoricalHyperparameter("param", ["b", "a"], weights=[1, 2], default_value="a") - assert c1 != c2 - - c1 = CategoricalHyperparameter("param", ["a", "b"], weights=[1, 2], default_value="a") - c2 = CategoricalHyperparameter("param", ["b", "a"], default_value="a") - assert c1 != c2 - - c1 = CategoricalHyperparameter("param", ["a", "b"], default_value="a") - c2 = CategoricalHyperparameter("param", ["b", "a"], weights=[1, 2], default_value="a") - assert c1 != c2 - - # Test that the equals operator does not fail accessing the weight of choice "a" in c2 - c1 = CategoricalHyperparameter("param", ["a", "b"], weights=[1, 2]) - c2 = CategoricalHyperparameter("param", ["b", "c"], weights=[1, 2]) - assert c1 != c2 - - def test_categorical_strings(self): - f1 = CategoricalHyperparameter("param", ["a", "b"]) - f1_ = CategoricalHyperparameter("param", ["a", "b"]) - assert f1 == f1_ - assert str(f1) == "param, Type: Categorical, Choices: {a, b}, Default: a" - - def test_categorical_is_legal(self): - f1 = CategoricalHyperparameter("param", ["a", "b"]) - assert f1.is_legal("a") - assert f1.is_legal("a") - assert not f1.is_legal("c") - assert not f1.is_legal(3) - - # Test is legal vector - assert f1.is_legal_vector(1.0) - assert f1.is_legal_vector(0.0) - assert f1.is_legal_vector(0) - assert not f1.is_legal_vector(0.3) - assert not f1.is_legal_vector(-0.1) - self.assertRaises(TypeError, f1.is_legal_vector, "Hahaha") - - def test_categorical_choices(self): - with self.assertRaisesRegex( - ValueError, - "Choices for categorical hyperparameters param contain choice 'a' 2 times, " - "while only a single oocurence is allowed.", - ): - CategoricalHyperparameter("param", ["a", "a"]) - - with self.assertRaisesRegex( - TypeError, - "Choice 'None' is not supported", - ): - CategoricalHyperparameter("param", ["a", None]) - - def test_categorical_default(self): - # Test that the default value is the most probable choice when weights are given - f1 = CategoricalHyperparameter("param", ["a", "b"]) - f2 = CategoricalHyperparameter("param", ["a", "b"], weights=[0.3, 0.6]) - f3 = CategoricalHyperparameter("param", ["a", "b"], weights=[0.6, 0.3]) - assert f1.default_value != f2.default_value - assert f1.default_value == f3.default_value - - def test_sample_UniformFloatHyperparameter(self): - # This can sample four distributions - def sample(hp): - rs = np.random.RandomState(1) - counts_per_bin = [0 for _ in range(21)] - value = None - for _ in range(100000): - value = hp.sample(rs) - if hp.log: - assert value <= np.exp(hp._upper) - assert value >= np.exp(hp._lower) - else: - assert value <= hp._upper - assert value >= hp._lower - index = int((value - hp.lower) / (hp.upper - hp.lower) * 20) - counts_per_bin[index] += 1 - - assert isinstance(value, float) - return counts_per_bin - - # Uniform - hp = UniformFloatHyperparameter("ufhp", 0.5, 2.5) - - counts_per_bin = sample(hp) - # The 21st bin is only filled if exactly 2.5 is sampled...very rare... - for bin in counts_per_bin[:-1]: - assert 5200 > bin > 4800 - assert sample(hp) == sample(hp) - - # Quantized Uniform - hp = UniformFloatHyperparameter("ufhp", 0.0, 1.0, q=0.1) - - counts_per_bin = sample(hp) - for bin in counts_per_bin[::2]: - assert 9301 > bin > 8700 - for bin in counts_per_bin[1::2]: - assert bin == 0 - assert sample(hp) == sample(hp) - - # Log Uniform - hp = UniformFloatHyperparameter("ufhp", 1.0, np.e**2, log=True) - - counts_per_bin = sample(hp) - assert counts_per_bin == [ - 14012, - 10977, - 8809, - 7559, - 6424, - 5706, - 5276, - 4694, - 4328, - 3928, - 3655, - 3386, - 3253, - 2932, - 2816, - 2727, - 2530, - 2479, - 2280, - 2229, - 0, - ] - assert sample(hp) == sample(hp) - - # Quantized Log-Uniform - # 7.2 ~ np.round(e * e, 1) - hp = UniformFloatHyperparameter("ufhp", 1.2, 7.2, q=0.6, log=True) - - counts_per_bin = sample(hp) - assert counts_per_bin == [ - 24359, - 15781, - 0, - 11635, - 0, - 0, - 9506, - 7867, - 0, - 0, - 6763, - 0, - 5919, - 0, - 5114, - 4798, - 0, - 0, - 4339, - 0, - 3919, - ] - assert sample(hp) == sample(hp) - - # Issue #199 - hp = UniformFloatHyperparameter("uni_float_q", lower=1e-4, upper=1e-1, q=1e-5, log=True) - assert np.isfinite(hp._lower) - assert np.isfinite(hp._upper) - sample(hp) - - def test_categorical_pdf(self): - c1 = CategoricalHyperparameter("x1", choices=["one", "two", "three"], weights=[2, 1, 2]) - c2 = CategoricalHyperparameter("x1", choices=["one", "two", "three"], weights=[5, 0, 2]) - c3 = CategoricalHyperparameter("x1", choices=["one", "two", "three", "four"]) - - point_1 = np.array(["one"]) - point_2 = np.array(["two"]) - - wrong_shape_1 = np.array([["one"]]) - wrong_shape_2 = np.array(["one", "two"]).reshape(1, -1) - wrong_shape_3 = np.array(["one", "two"]).reshape(-1, 1) - - assert c1.pdf(point_1)[0] == 0.4 - assert c1.pdf(point_2)[0] == 0.2 - self.assertAlmostEqual(c2.pdf(point_1)[0], 0.7142857142857143) - assert c2.pdf(point_2)[0] == 0.0 - assert c3.pdf(point_1)[0] == 0.25 - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1.pdf(0.2) - with self.assertRaises(TypeError): - c1.pdf("pdf") - with self.assertRaises(TypeError): - c1.pdf("one") - with self.assertRaises(ValueError): - c1.pdf(np.array(["zero"])) - - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_1) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_2) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_3) - - def test_categorical__pdf(self): - c1 = CategoricalHyperparameter("x1", choices=["one", "two", "three"], weights=[2, 1, 2]) - c2 = CategoricalHyperparameter("x1", choices=["one", "two", "three"], weights=[5, 0, 2]) - - point_1 = np.array([0]) - point_2 = np.array([1]) - array_1 = np.array([1, 0, 2]) - nan = np.array([0, np.nan]) - assert c1._pdf(point_1)[0] == 0.4 - assert c1._pdf(point_2)[0] == 0.2 - self.assertAlmostEqual(c2._pdf(point_1)[0], 0.7142857142857143) - assert c2._pdf(point_2)[0] == 0.0 - - array_results = c1._pdf(array_1) - expected_results = np.array([0.2, 0.4, 0.4]) - assert array_results.shape == expected_results.shape - for res, exp_res in zip(array_results, expected_results): - assert res == exp_res - - nan_results = c1._pdf(nan) - expected_results = np.array([0.4, 0]) - assert nan_results.shape == expected_results.shape - for res, exp_res in zip(nan_results, expected_results): - assert res == exp_res - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1._pdf(0.2) - with self.assertRaises(TypeError): - c1._pdf("pdf") - with self.assertRaises(TypeError): - c1._pdf("one") - with self.assertRaises(TypeError): - c1._pdf(np.array(["zero"])) - - def test_categorical_get_max_density(self): - c1 = CategoricalHyperparameter("x1", choices=["one", "two", "three"], weights=[2, 1, 2]) - c2 = CategoricalHyperparameter("x1", choices=["one", "two", "three"], weights=[5, 0, 2]) - c3 = CategoricalHyperparameter("x1", choices=["one", "two", "three"]) - assert c1.get_max_density() == 0.4 - assert c2.get_max_density() == 0.7142857142857143 - self.assertAlmostEqual(c3.get_max_density(), 0.33333333333333) - - def test_sample_NormalFloatHyperparameter(self): - hp = NormalFloatHyperparameter("nfhp", 0, 1) - - def actual_test(): - rs = np.random.RandomState(1) - counts_per_bin = [0 for _ in range(11)] - value = None - for _ in range(100000): - value = hp.sample(rs) - index = min(max(int((np.round(value + 0.5)) + 5), 0), 9) - counts_per_bin[index] += 1 - - assert [0, 4, 138, 2113, 13394, 34104, 34282, 13683, 2136, 146, 0] == counts_per_bin - - assert isinstance(value, float) - return counts_per_bin - - assert actual_test() == actual_test() - - def test_sample_NormalFloatHyperparameter_with_bounds(self): - hp = NormalFloatHyperparameter("nfhp", 0, 1, lower=-3, upper=3) - - def actual_test(): - rs = np.random.RandomState(1) - counts_per_bin = [0 for _ in range(11)] - value = None - for _ in range(100000): - value = hp.sample(rs) - index = min(max(int((np.round(value + 0.5)) + 5), 0), 9) - counts_per_bin[index] += 1 - - assert [0, 0, 0, 2184, 13752, 34078, 34139, 13669, 2178, 0, 0] == counts_per_bin - - assert isinstance(value, float) - return counts_per_bin - - assert actual_test() == actual_test() - - def test_sample_BetaFloatHyperparameter(self): - hp = BetaFloatHyperparameter("bfhp", alpha=8, beta=1.5, lower=-1, upper=10) - - def actual_test(): - rs = np.random.RandomState(1) - counts_per_bin = [0 for _ in range(11)] - value = None - for _ in range(1000): - value = hp.sample(rs) - index = np.floor(value).astype(int) - counts_per_bin[index] += 1 - - assert [0, 2, 2, 4, 15, 39, 101, 193, 289, 355, 0] == counts_per_bin - - assert isinstance(value, float) - return counts_per_bin - - assert actual_test() == actual_test() - - def test_sample_UniformIntegerHyperparameter(self): - # TODO: disentangle, actually test _sample and test sample on the - # base class - def sample(hp): - rs = np.random.RandomState(1) - counts_per_bin = [0 for _ in range(21)] - values = [] - value = None - for _ in range(100000): - value = hp.sample(rs) - values.append(value) - index = int(float(value - hp.lower) / (hp.upper - hp.lower) * 20) - counts_per_bin[index] += 1 - - assert isinstance(value, int) - return counts_per_bin - - # Quantized Uniform - hp = UniformIntegerHyperparameter("uihp", 0, 10) - - counts_per_bin = sample(hp) - for bin in counts_per_bin[::2]: - assert 9302 > bin > 8700 - for bin in counts_per_bin[1::2]: - assert bin == 0 - assert sample(hp) == sample(hp) - - def test__sample_UniformIntegerHyperparameter(self): - hp = UniformIntegerHyperparameter("uihp", 0, 10) - values = [] - rs = np.random.RandomState(1) - for _ in range(100): - values.append(hp._sample(rs)) - assert len(np.unique(values)) == 11 - - hp = UniformIntegerHyperparameter("uihp", 2, 12) - values = [] - rs = np.random.RandomState(1) - for _ in range(100): - values.append(hp._sample(rs)) - assert hp._transform(values[-1]) >= 2 - assert hp._transform(values[-1]) <= 12 - assert len(np.unique(values)) == 11 - - def test_quantization_UniformIntegerHyperparameter(self): - hp = UniformIntegerHyperparameter("uihp", 1, 100, q=3) - rs = np.random.RandomState() - - sample_one = hp._sample(rs=rs, size=1) - assert isinstance(sample_one, np.ndarray) - assert sample_one.size == 1 - assert (hp._transform(sample_one) - 1) % 3 == 0 - assert hp._transform(sample_one) >= 1 - assert hp._transform(sample_one) <= 100 - - sample_hundred = hp._sample(rs=rs, size=100) - assert isinstance(sample_hundred, np.ndarray) - assert sample_hundred.size == 100 - np.testing.assert_array_equal( - [(hp._transform(val) - 1) % 3 for val in sample_hundred], - np.zeros((100,), dtype=int), - ) - samples_in_original_space = hp._transform(sample_hundred) - for i in range(100): - assert samples_in_original_space[i] >= 1 - assert samples_in_original_space[i] <= 100 - - def test_quantization_UniformIntegerHyperparameter_negative(self): - hp = UniformIntegerHyperparameter("uihp", -2, 100, q=3) - rs = np.random.RandomState() - - sample_one = hp._sample(rs=rs, size=1) - assert isinstance(sample_one, np.ndarray) - assert sample_one.size == 1 - assert (hp._transform(sample_one) + 2) % 3 == 0 - assert hp._transform(sample_one) >= -2 - assert hp._transform(sample_one) <= 100 - - sample_hundred = hp._sample(rs=rs, size=100) - assert isinstance(sample_hundred, np.ndarray) - assert sample_hundred.size == 100 - np.testing.assert_array_equal( - [(hp._transform(val) + 2) % 3 for val in sample_hundred], - np.zeros((100,), dtype=int), - ) - samples_in_original_space = hp._transform(sample_hundred) - for i in range(100): - assert samples_in_original_space[i] >= -2 - assert samples_in_original_space[i] <= 100 - - def test_illegal_quantization_UniformIntegerHyperparameter(self): - with self.assertRaisesRegex( - ValueError, - r"Upper bound \(4\) - lower bound \(1\) must be a multiple of q \(2\)", - ): - UniformIntegerHyperparameter("uihp", 1, 4, q=2) - - def test_quantization_UniformFloatHyperparameter(self): - hp = UniformFloatHyperparameter("ufhp", 1, 100, q=3) - rs = np.random.RandomState() - - sample_one = hp._sample(rs=rs, size=1) - assert isinstance(sample_one, np.ndarray) - assert sample_one.size == 1 - assert (hp._transform(sample_one) - 1) % 3 == 0 - assert hp._transform(sample_one) >= 1 - assert hp._transform(sample_one) <= 100 - - sample_hundred = hp._sample(rs=rs, size=100) - assert isinstance(sample_hundred, np.ndarray) - assert sample_hundred.size == 100 - np.testing.assert_array_equal( - [(hp._transform(val) - 1) % 3 for val in sample_hundred], - np.zeros((100,), dtype=int), - ) - samples_in_original_space = hp._transform(sample_hundred) - for i in range(100): - assert samples_in_original_space[i] >= 1 - assert samples_in_original_space[i] <= 100 - - def test_quantization_UniformFloatHyperparameter_decimal_numbers(self): - hp = UniformFloatHyperparameter("ufhp", 1.2, 3.6, q=0.2) - rs = np.random.RandomState() - - sample_one = hp._sample(rs=rs, size=1) - assert isinstance(sample_one, np.ndarray) - assert sample_one.size == 1 - try: - self.assertAlmostEqual(float(hp._transform(sample_one) + 1.2) % 0.2, 0.0) - except Exception: - self.assertAlmostEqual(float(hp._transform(sample_one) + 1.2) % 0.2, 0.2) - assert hp._transform(sample_one) >= 1 - assert hp._transform(sample_one) <= 100 - - def test_quantization_UniformFloatHyperparameter_decimal_numbers_negative(self): - hp = UniformFloatHyperparameter("ufhp", -1.2, 1.2, q=0.2) - rs = np.random.RandomState() - - sample_one = hp._sample(rs=rs, size=1) - assert isinstance(sample_one, np.ndarray) - assert sample_one.size == 1 - try: - self.assertAlmostEqual(float(hp._transform(sample_one) + 1.2) % 0.2, 0.0) - except Exception: - self.assertAlmostEqual(float(hp._transform(sample_one) + 1.2) % 0.2, 0.2) - assert hp._transform(sample_one) >= -1.2 - assert hp._transform(sample_one) <= 1.2 - - def test_sample_NormalIntegerHyperparameter(self): - def sample(hp): - lower = -30 - upper = 30 - rs = np.random.RandomState(1) - counts_per_bin = [0 for _ in range(21)] - value = None - for _ in range(100000): - value = hp.sample(rs) - sample = float(value) - if sample < lower: - sample = lower - if sample > upper: - sample = upper - index = int((sample - lower) / (upper - lower) * 20) - counts_per_bin[index] += 1 - - assert isinstance(value, int) - return counts_per_bin - - hp = NormalIntegerHyperparameter("nihp", 0, 10) - assert sample(hp) == [ - 305, - 422, - 835, - 1596, - 2682, - 4531, - 6572, - 8670, - 10649, - 11510, - 11854, - 11223, - 9309, - 7244, - 5155, - 3406, - 2025, - 1079, - 514, - 249, - 170, - ] - assert sample(hp) == sample(hp) - - def test__sample_NormalIntegerHyperparameter(self): - # mean zero, std 1 - hp = NormalIntegerHyperparameter("uihp", 0, 1) - values = [] + def actual_test(): rs = np.random.RandomState(1) - for _ in range(100): - values.append(hp._sample(rs)) - assert len(np.unique(values)) == 5 - - def test_sample_BetaIntegerHyperparameter(self): - hp = BetaIntegerHyperparameter("bihp", alpha=4, beta=4, lower=0, upper=10) - - def actual_test(): - rs = np.random.RandomState(1) - counts_per_bin = [0 for _ in range(11)] - for _ in range(1000): - value = hp.sample(rs) - counts_per_bin[value] += 1 - - # The chosen distribution is symmetric, so we expect to see a symmetry in the bins - assert [1, 23, 82, 121, 174, 197, 174, 115, 86, 27, 0] == counts_per_bin + counts_per_bin = [0 for _ in range(11)] + value = None + for _ in range(1000): + value = hp.sample(rs) + index = np.floor(value).astype(int) + counts_per_bin[index] += 1 - return counts_per_bin + assert [0, 2, 2, 4, 15, 39, 101, 193, 289, 355, 0] == counts_per_bin - assert actual_test() == actual_test() + assert isinstance(value, float) + return counts_per_bin - def test_sample_CategoricalHyperparameter(self): - hp = CategoricalHyperparameter("chp", [0, 2, "Bla", "Blub"]) + assert actual_test() == actual_test() - def actual_test(): - rs = np.random.RandomState(1) - counts_per_bin: dict[str, int] = defaultdict(int) - for _ in range(10000): - value = hp.sample(rs) - counts_per_bin[value] += 1 - assert {0: 2539, 2: 2451, "Bla": 2549, "Blub": 2461} == dict(counts_per_bin.items()) - return counts_per_bin +def test_sample_UniformIntegerHyperparameter(): + # TODO: disentangle, actually test _sample and test sample on the + # base class + def sample(hp): + rs = np.random.RandomState(1) + counts_per_bin = [0 for _ in range(21)] + values = [] + value = None + for _ in range(100000): + value = hp.sample(rs) + values.append(value) + index = int(float(value - hp.lower) / (hp.upper - hp.lower) * 20) + counts_per_bin[index] += 1 + + assert isinstance(value, int) + return counts_per_bin + + # Quantized Uniform + hp = UniformIntegerHyperparameter("uihp", 0, 10) + + counts_per_bin = sample(hp) + for bin in counts_per_bin[::2]: + assert 9302 > bin > 8700 + for bin in counts_per_bin[1::2]: + assert bin == 0 + assert sample(hp) == sample(hp) + + +def test__sample_UniformIntegerHyperparameter(): + hp = UniformIntegerHyperparameter("uihp", 0, 10) + values = [] + rs = np.random.RandomState(1) + for _ in range(100): + values.append(hp._sample(rs)) + assert len(np.unique(values)) == 11 + + hp = UniformIntegerHyperparameter("uihp", 2, 12) + values = [] + rs = np.random.RandomState(1) + for _ in range(100): + values.append(hp._sample(rs)) + assert hp._transform(values[-1]) >= 2 + assert hp._transform(values[-1]) <= 12 + assert len(np.unique(values)) == 11 + + +def test_quantization_UniformIntegerHyperparameter(): + hp = UniformIntegerHyperparameter("uihp", 1, 100, q=3) + rs = np.random.RandomState() + + sample_one = hp._sample(rs=rs, size=1) + assert isinstance(sample_one, np.ndarray) + assert sample_one.size == 1 + assert (hp._transform(sample_one) - 1) % 3 == 0 + assert hp._transform(sample_one) >= 1 + assert hp._transform(sample_one) <= 100 + + sample_hundred = hp._sample(rs=rs, size=100) + assert isinstance(sample_hundred, np.ndarray) + assert sample_hundred.size == 100 + np.testing.assert_array_equal( + [(hp._transform(val) - 1) % 3 for val in sample_hundred], + np.zeros((100,), dtype=int), + ) + samples_in_original_space = hp._transform(sample_hundred) + for i in range(100): + assert samples_in_original_space[i] >= 1 + assert samples_in_original_space[i] <= 100 + + +def test_quantization_UniformIntegerHyperparameter_negative(): + hp = UniformIntegerHyperparameter("uihp", -2, 100, q=3) + rs = np.random.RandomState() + + sample_one = hp._sample(rs=rs, size=1) + assert isinstance(sample_one, np.ndarray) + assert sample_one.size == 1 + assert (hp._transform(sample_one) + 2) % 3 == 0 + assert hp._transform(sample_one) >= -2 + assert hp._transform(sample_one) <= 100 + + sample_hundred = hp._sample(rs=rs, size=100) + assert isinstance(sample_hundred, np.ndarray) + assert sample_hundred.size == 100 + np.testing.assert_array_equal( + [(hp._transform(val) + 2) % 3 for val in sample_hundred], + np.zeros((100,), dtype=int), + ) + samples_in_original_space = hp._transform(sample_hundred) + for i in range(100): + assert samples_in_original_space[i] >= -2 + assert samples_in_original_space[i] <= 100 + + +def test_illegal_quantization_UniformIntegerHyperparameter(): + with pytest.raises( + ValueError, + match=r"Upper bound \(4\) - lower bound \(1\) must be a multiple of q \(2\)", + ): + UniformIntegerHyperparameter("uihp", 1, 4, q=2) + + +def test_quantization_UniformFloatHyperparameter(): + hp = UniformFloatHyperparameter("ufhp", 1, 100, q=3) + rs = np.random.RandomState() + + sample_one = hp._sample(rs=rs, size=1) + assert isinstance(sample_one, np.ndarray) + assert sample_one.size == 1 + assert (hp._transform(sample_one) - 1) % 3 == 0 + assert hp._transform(sample_one) >= 1 + assert hp._transform(sample_one) <= 100 + + sample_hundred = hp._sample(rs=rs, size=100) + assert isinstance(sample_hundred, np.ndarray) + assert sample_hundred.size == 100 + np.testing.assert_array_equal( + [(hp._transform(val) - 1) % 3 for val in sample_hundred], + np.zeros((100,), dtype=int), + ) + samples_in_original_space = hp._transform(sample_hundred) + for i in range(100): + assert samples_in_original_space[i] >= 1 + assert samples_in_original_space[i] <= 100 + + +def test_quantization_UniformFloatHyperparameter_decimal_numbers(): + hp = UniformFloatHyperparameter("ufhp", 1.2, 3.6, q=0.2) + rs = np.random.RandomState() + + sample_one = hp._sample(rs=rs, size=1) + assert isinstance(sample_one, np.ndarray) + assert sample_one.size == 1 + try: + assert float(hp._transform(sample_one) + 1.2) % 0.2 == pytest.approx(0.0) + except Exception: + assert float(hp._transform(sample_one) + 1.2) % 0.2 == pytest.approx(0.2) + assert hp._transform(sample_one) >= 1 + assert hp._transform(sample_one) <= 100 + + +def test_quantization_UniformFloatHyperparameter_decimal_numbers_negative(): + hp = UniformFloatHyperparameter("ufhp", -1.2, 1.2, q=0.2) + rs = np.random.RandomState() + + sample_one = hp._sample(rs=rs, size=1) + assert isinstance(sample_one, np.ndarray) + assert sample_one.size == 1 + try: + assert float(hp._transform(sample_one) + 1.2) % 0.2 == pytest.approx(0.0) + except Exception: + assert float(hp._transform(sample_one) + 1.2) % 0.2 == pytest.approx(0.2) + assert hp._transform(sample_one) >= -1.2 + assert hp._transform(sample_one) <= 1.2 + + +def test_sample_NormalIntegerHyperparameter(): + def sample(hp): + lower = -30 + upper = 30 + rs = np.random.RandomState(1) + counts_per_bin = [0 for _ in range(21)] + value = None + for _ in range(100000): + value = hp.sample(rs) + sample = float(value) + if sample < lower: + sample = lower + if sample > upper: + sample = upper + index = int((sample - lower) / (upper - lower) * 20) + counts_per_bin[index] += 1 + + assert isinstance(value, int) + return counts_per_bin + + hp = NormalIntegerHyperparameter("nihp", 0, 10) + assert sample(hp) == [ + 305, + 422, + 835, + 1596, + 2682, + 4531, + 6572, + 8670, + 10649, + 11510, + 11854, + 11223, + 9309, + 7244, + 5155, + 3406, + 2025, + 1079, + 514, + 249, + 170, + ] + assert sample(hp) == sample(hp) + + +def test__sample_NormalIntegerHyperparameter(): + # mean zero, std 1 + hp = NormalIntegerHyperparameter("uihp", 0, 1) + values = [] + rs = np.random.RandomState(1) + for _ in range(100): + values.append(hp._sample(rs)) + assert len(np.unique(values)) == 5 + + +def test_sample_BetaIntegerHyperparameter(): + hp = BetaIntegerHyperparameter("bihp", alpha=4, beta=4, lower=0, upper=10) + + def actual_test(): + rs = np.random.RandomState(1) + counts_per_bin = [0 for _ in range(11)] + for _ in range(1000): + value = hp.sample(rs) + counts_per_bin[value] += 1 - assert actual_test() == actual_test() + # The chosen distribution is symmetric, so we expect to see a symmetry in the bins + assert [1, 23, 82, 121, 174, 197, 174, 115, 86, 27, 0] == counts_per_bin - def test_sample_CategoricalHyperparameter_with_weights(self): - # check also that normalization works - hp = CategoricalHyperparameter( - "chp", - [0, 2, "Bla", "Blub", "Blurp"], - weights=[1, 2, 3, 4, 0], - ) - np.testing.assert_almost_equal( - actual=hp.probabilities, - desired=[0.1, 0.2, 0.3, 0.4, 0], - decimal=3, - ) + return counts_per_bin - def actual_test(): - rs = np.random.RandomState(1) - counts_per_bin: dict[str | int, int] = defaultdict(int) - for _ in range(10000): - value = hp.sample(rs) - counts_per_bin[value] += 1 + assert actual_test() == actual_test() - assert {0: 1003, 2: 2061, "Bla": 2994, "Blub": 3942} == dict(counts_per_bin.items()) - return counts_per_bin - assert actual_test() == actual_test() +def test_sample_CategoricalHyperparameter(): + hp = CategoricalHyperparameter("chp", [0, 2, "Bla", "Blub"]) - def test_categorical_copy_with_weights(self): - orig_hp = CategoricalHyperparameter( + def actual_test(): + rs = np.random.RandomState(1) + counts_per_bin: dict[str, int] = defaultdict(int) + for _ in range(10000): + value = hp.sample(rs) + counts_per_bin[value] += 1 + + assert {0: 2539, 2: 2451, "Bla": 2549, "Blub": 2461} == dict(counts_per_bin.items()) + return counts_per_bin + + assert actual_test() == actual_test() + + +def test_sample_CategoricalHyperparameter_with_weights(): + # check also that normalization works + hp = CategoricalHyperparameter( + "chp", + [0, 2, "Bla", "Blub", "Blurp"], + weights=[1, 2, 3, 4, 0], + ) + np.testing.assert_almost_equal( + actual=hp.probabilities, + desired=[0.1, 0.2, 0.3, 0.4, 0], + decimal=3, + ) + + def actual_test(): + rs = np.random.RandomState(1) + counts_per_bin: dict[str | int, int] = defaultdict(int) + for _ in range(10000): + value = hp.sample(rs) + counts_per_bin[value] += 1 + + assert {0: 1003, 2: 2061, "Bla": 2994, "Blub": 3942} == dict(counts_per_bin.items()) + return counts_per_bin + + assert actual_test() == actual_test() + + +def test_categorical_copy_with_weights(): + orig_hp = CategoricalHyperparameter( + name="param", + choices=[1, 2, 3], + default_value=2, + weights=[1, 3, 6], + ) + copy_hp = copy.copy(orig_hp) + + assert copy_hp.name == orig_hp.name + assert copy_hp.choices == orig_hp.choices + assert copy_hp.default_value == orig_hp.default_value + assert copy_hp.num_choices == orig_hp.num_choices + assert copy_hp.probabilities == orig_hp.probabilities + + +def test_categorical_copy_without_weights(): + orig_hp = CategoricalHyperparameter(name="param", choices=[1, 2, 3], default_value=2) + copy_hp = copy.copy(orig_hp) + + assert copy_hp.name == orig_hp.name + assert copy_hp.choices == orig_hp.choices + assert copy_hp.default_value == orig_hp.default_value + assert copy_hp.num_choices == orig_hp.num_choices + assert copy_hp.probabilities == (0.3333333333333333, 0.3333333333333333, 0.3333333333333333) + assert orig_hp.probabilities == (0.3333333333333333, 0.3333333333333333, 0.3333333333333333) + + +def test_categorical_with_weights(): + rs = np.random.RandomState() + + cat_hp_str = CategoricalHyperparameter( + name="param", + choices=["A", "B", "C"], + default_value="A", + weights=[0.1, 0.6, 0.3], + ) + for _ in range(1000): + assert cat_hp_str.sample(rs) in ["A", "B", "C"] + + cat_hp_int = CategoricalHyperparameter( + name="param", + choices=[1, 2, 3], + default_value=2, + weights=[0.1, 0.3, 0.6], + ) + for _ in range(1000): + assert cat_hp_int.sample(rs) in [1, 3, 2] + + cat_hp_float = CategoricalHyperparameter( + name="param", + choices=[-0.1, 0.0, 0.3], + default_value=0.3, + weights=[10, 60, 30], + ) + for _ in range(1000): + assert cat_hp_float.sample(rs) in [-0.1, 0.0, 0.3] + + +def test_categorical_with_some_zero_weights(): + # zero weights are okay as long as there is at least one strictly positive weight + + rs = np.random.RandomState() + + cat_hp_str = CategoricalHyperparameter( + name="param", + choices=["A", "B", "C"], + default_value="A", + weights=[0.1, 0.0, 0.3], + ) + for _ in range(1000): + assert cat_hp_str.sample(rs) in ["A", "C"] + np.testing.assert_almost_equal( + actual=cat_hp_str.probabilities, + desired=[0.25, 0.0, 0.75], + decimal=3, + ) + + cat_hp_int = CategoricalHyperparameter( + name="param", + choices=[1, 2, 3], + default_value=2, + weights=[0.1, 0.6, 0.0], + ) + for _ in range(1000): + assert cat_hp_int.sample(rs) in [1, 2] + np.testing.assert_almost_equal( + actual=cat_hp_int.probabilities, + desired=[0.1429, 0.8571, 0.0], + decimal=3, + ) + + cat_hp_float = CategoricalHyperparameter( + name="param", + choices=[-0.1, 0.0, 0.3], + default_value=0.3, + weights=[0.0, 0.6, 0.3], + ) + for _ in range(1000): + assert cat_hp_float.sample(rs) in [0.0, 0.3] + np.testing.assert_almost_equal( + actual=cat_hp_float.probabilities, + desired=[0.00, 0.6667, 0.3333], + decimal=3, + ) + + +def test_categorical_with_all_zero_weights(): + with pytest.raises(ValueError, match="At least one weight has to be strictly positive."): + CategoricalHyperparameter( name="param", - choices=[1, 2, 3], - default_value=2, - weights=[1, 3, 6], - ) - copy_hp = copy.copy(orig_hp) - - assert copy_hp.name == orig_hp.name - self.assertTupleEqual(copy_hp.choices, orig_hp.choices) - assert copy_hp.default_value == orig_hp.default_value - assert copy_hp.num_choices == orig_hp.num_choices - self.assertTupleEqual(copy_hp.probabilities, orig_hp.probabilities) - - def test_categorical_copy_without_weights(self): - orig_hp = CategoricalHyperparameter(name="param", choices=[1, 2, 3], default_value=2) - copy_hp = copy.copy(orig_hp) - - assert copy_hp.name == orig_hp.name - self.assertTupleEqual(copy_hp.choices, orig_hp.choices) - assert copy_hp.default_value == orig_hp.default_value - assert copy_hp.num_choices == orig_hp.num_choices - self.assertTupleEqual( - copy_hp.probabilities, - (0.3333333333333333, 0.3333333333333333, 0.3333333333333333), - ) - self.assertTupleEqual( - orig_hp.probabilities, - (0.3333333333333333, 0.3333333333333333, 0.3333333333333333), + choices=["A", "B", "C"], + default_value="A", + weights=[0.0, 0.0, 0.0], ) - def test_categorical_with_weights(self): - rs = np.random.RandomState() - cat_hp_str = CategoricalHyperparameter( +def test_categorical_with_wrong_length_weights(): + with pytest.raises( + ValueError, + match="The list of weights and the list of choices are required to be of same length.", + ): + CategoricalHyperparameter( name="param", choices=["A", "B", "C"], default_value="A", - weights=[0.1, 0.6, 0.3], + weights=[0.1, 0.3], ) - for _ in range(1000): - assert cat_hp_str.sample(rs) in ["A", "B", "C"] - - cat_hp_int = CategoricalHyperparameter( - name="param", - choices=[1, 2, 3], - default_value=2, - weights=[0.1, 0.3, 0.6], - ) - for _ in range(1000): - assert cat_hp_int.sample(rs) in [1, 3, 2] - cat_hp_float = CategoricalHyperparameter( + with pytest.raises( + ValueError, + match="The list of weights and the list of choices are required to be of same length.", + ): + CategoricalHyperparameter( name="param", - choices=[-0.1, 0.0, 0.3], - default_value=0.3, - weights=[10, 60, 30], + choices=["A", "B", "C"], + default_value="A", + weights=[0.1, 0.0, 0.5, 0.3], ) - for _ in range(1000): - assert cat_hp_float.sample(rs) in [-0.1, 0.0, 0.3] - - def test_categorical_with_some_zero_weights(self): - # zero weights are okay as long as there is at least one strictly positive weight - rs = np.random.RandomState() - cat_hp_str = CategoricalHyperparameter( +def test_categorical_with_negative_weights(): + with pytest.raises(ValueError, match="Negative weights are not allowed."): + CategoricalHyperparameter( name="param", choices=["A", "B", "C"], default_value="A", - weights=[0.1, 0.0, 0.3], - ) - for _ in range(1000): - assert cat_hp_str.sample(rs) in ["A", "C"] - np.testing.assert_almost_equal( - actual=cat_hp_str.probabilities, - desired=[0.25, 0.0, 0.75], - decimal=3, + weights=[0.1, -0.1, 0.3], ) - cat_hp_int = CategoricalHyperparameter( + +def test_categorical_with_set(): + with pytest.raises(TypeError, match="Using a set of choices is prohibited."): + CategoricalHyperparameter( name="param", - choices=[1, 2, 3], - default_value=2, - weights=[0.1, 0.6, 0.0], - ) - for _ in range(1000): - assert cat_hp_int.sample(rs) in [1, 2] - np.testing.assert_almost_equal( - actual=cat_hp_int.probabilities, - desired=[0.1429, 0.8571, 0.0], - decimal=3, + choices={"A", "B", "C"}, + default_value="A", ) - cat_hp_float = CategoricalHyperparameter( + with pytest.raises(TypeError, match="Using a set of weights is prohibited."): + CategoricalHyperparameter( name="param", - choices=[-0.1, 0.0, 0.3], - default_value=0.3, - weights=[0.0, 0.6, 0.3], - ) - for _ in range(1000): - assert cat_hp_float.sample(rs) in [0.0, 0.3] - np.testing.assert_almost_equal( - actual=cat_hp_float.probabilities, - desired=[0.00, 0.6667, 0.3333], - decimal=3, + choices=["A", "B", "C"], + default_value="A", + weights={0.2, 0.6, 0.8}, ) - def test_categorical_with_all_zero_weights(self): - with self.assertRaisesRegex(ValueError, "At least one weight has to be strictly positive."): - CategoricalHyperparameter( - name="param", - choices=["A", "B", "C"], - default_value="A", - weights=[0.0, 0.0, 0.0], - ) - - def test_categorical_with_wrong_length_weights(self): - with self.assertRaisesRegex( - ValueError, - "The list of weights and the list of choices are required to be of same length.", - ): - CategoricalHyperparameter( - name="param", - choices=["A", "B", "C"], - default_value="A", - weights=[0.1, 0.3], - ) - - with self.assertRaisesRegex( - ValueError, - "The list of weights and the list of choices are required to be of same length.", - ): - CategoricalHyperparameter( - name="param", - choices=["A", "B", "C"], - default_value="A", - weights=[0.1, 0.0, 0.5, 0.3], - ) - - def test_categorical_with_negative_weights(self): - with self.assertRaisesRegex(ValueError, "Negative weights are not allowed."): - CategoricalHyperparameter( - name="param", - choices=["A", "B", "C"], - default_value="A", - weights=[0.1, -0.1, 0.3], - ) - - def test_categorical_with_set(self): - with self.assertRaisesRegex(TypeError, "Using a set of choices is prohibited."): - CategoricalHyperparameter( - name="param", - choices={"A", "B", "C"}, - default_value="A", - ) - - with self.assertRaisesRegex(TypeError, "Using a set of weights is prohibited."): - CategoricalHyperparameter( - name="param", - choices=["A", "B", "C"], - default_value="A", - weights={0.2, 0.6, 0.8}, - ) - - def test_log_space_conversion(self): - lower, upper = 1e-5, 1e5 - hyper = UniformFloatHyperparameter("test", lower=lower, upper=upper, log=True) - assert hyper.is_legal(hyper._transform(1.0)) - - lower, upper = 1e-10, 1e10 - hyper = UniformFloatHyperparameter("test", lower=lower, upper=upper, log=True) - assert hyper.is_legal(hyper._transform(1.0)) - - def test_ordinal_attributes_accessible(self): - f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) - assert f1.name == "temp" - self.assertTupleEqual(f1.sequence, ("freezing", "cold", "warm", "hot")) - assert f1.num_elements == 4 - assert f1.default_value == "freezing" - assert f1.normalized_default_value == 0 - - def test_ordinal_is_legal(self): - f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) - assert f1.is_legal("warm") - assert f1.is_legal("freezing") - assert not f1.is_legal("chill") - assert not f1.is_legal(2.5) - assert not f1.is_legal("3") - - # Test is legal vector - assert f1.is_legal_vector(1.0) - assert f1.is_legal_vector(0.0) - assert f1.is_legal_vector(0) - assert f1.is_legal_vector(3) - assert not f1.is_legal_vector(-0.1) - self.assertRaises(TypeError, f1.is_legal_vector, "Hahaha") - - def test_ordinal_check_order(self): - f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) - assert f1.check_order("freezing", "cold") - assert f1.check_order("freezing", "hot") - assert not f1.check_order("hot", "cold") - assert not f1.check_order("hot", "warm") - - def test_ordinal_get_value(self): - f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) - assert f1.get_value(3) == "hot" - assert f1.get_value(1) != "warm" - - def test_ordinal_get_order(self): - f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) - assert f1.get_order("warm") == 2 - assert f1.get_order("freezing") != 3 - - def test_ordinal_get_seq_order(self): - f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) - assert tuple(f1.get_seq_order()) == (0, 1, 2, 3) - - def test_ordinal_get_neighbors(self): - f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) - assert f1.get_neighbors(0, rs=None) == [1] - assert f1.get_neighbors(1, rs=None) == [0, 2] - assert f1.get_neighbors(3, rs=None) == [2] - assert f1.get_neighbors("hot", transform=True, rs=None) == ["warm"] - assert f1.get_neighbors("cold", transform=True, rs=None) == ["freezing", "warm"] - - def test_get_num_neighbors(self): - f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) - assert f1.get_num_neighbors("freezing") == 1 - assert f1.get_num_neighbors("hot") == 1 - assert f1.get_num_neighbors("cold") == 2 - - def test_ordinal_get_size(self): - f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) - assert f1.get_size() == 4 - - def test_ordinal_pdf(self): - c1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) - point_1 = np.array(["freezing"]) - point_2 = np.array(["warm"]) - array_1 = np.array(["freezing", "warm"]) - - wrong_shape_1 = np.array([["freezing"]]) - wrong_shape_2 = np.array(["freezing", "warm"]).reshape(1, -1) - wrong_shape_3 = np.array(["freezing", "warm"]).reshape(-1, 1) - - assert c1.pdf(point_1)[0] == 0.25 - assert c1.pdf(point_2)[0] == 0.25 - - array_results = c1.pdf(array_1) - expected_results = np.array([0.25, 0.25]) - assert array_results.shape == expected_results.shape - for res, exp_res in zip(array_results, expected_results): - assert res == exp_res - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1.pdf(0.2) - with self.assertRaises(TypeError): - c1.pdf("pdf") - with self.assertRaises(TypeError): - c1.pdf("one") - with self.assertRaises(ValueError): - c1.pdf(np.array(["zero"])) - - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_1) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_2) - with self.assertRaisesRegex(ValueError, "Method pdf expects a one-dimensional numpy array"): - c1.pdf(wrong_shape_3) - - def test_ordinal__pdf(self): - c1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) - point_1 = np.array(["freezing"]) - point_2 = np.array(["warm"]) - array_1 = np.array(["freezing", "warm"]) - assert c1._pdf(point_1)[0] == 0.25 - assert c1._pdf(point_2)[0] == 0.25 - - array_results = c1._pdf(array_1) - expected_results = np.array([0.25, 0.25]) - assert array_results.shape == expected_results.shape - for res, exp_res in zip(array_results, expected_results): - assert res == exp_res - - # pdf must take a numpy array - with self.assertRaises(TypeError): - c1._pdf(0.2) - with self.assertRaises(TypeError): - c1._pdf("pdf") - with self.assertRaises(TypeError): - c1._pdf("one") - with self.assertRaises(ValueError): - c1._pdf(np.array(["zero"])) - - def test_ordinal_get_max_density(self): - c1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) - c2 = OrdinalHyperparameter("temp", ["freezing", "cold"]) - assert c1.get_max_density() == 0.25 - assert c2.get_max_density() == 0.5 - - def test_rvs(self): - f1 = UniformFloatHyperparameter("param", 0, 10) - - # test that returned types are correct - # if size=None, return a value, but if size=1, return a 1-element array - assert isinstance(f1.rvs(), float) - assert isinstance(f1.rvs(size=1), np.ndarray) - assert isinstance(f1.rvs(size=2), np.ndarray) - - self.assertAlmostEqual(f1.rvs(random_state=100), f1.rvs(random_state=100)) - self.assertAlmostEqual( - f1.rvs(random_state=100), - f1.rvs(random_state=np.random.RandomState(100)), - ) - f1.rvs(random_state=np.random) - f1.rvs(random_state=np.random.default_rng(1)) - self.assertRaises(ValueError, f1.rvs, 1, "a") - - def test_hyperparam_representation(self): - # Float - f1 = UniformFloatHyperparameter("param", 1, 100, log=True) - assert ( - repr(f1) - == "param, Type: UniformFloat, Range: [1.0, 100.0], Default: 10.0, on log-scale" - ) - f2 = NormalFloatHyperparameter("param", 8, 99.1, log=False) - assert repr(f2) == "param, Type: NormalFloat, Mu: 8.0 Sigma: 99.1, Default: 8.0" - f3 = NormalFloatHyperparameter("param", 8, 99.1, log=False, lower=1, upper=16) - assert ( - repr(f3) - == "param, Type: NormalFloat, Mu: 8.0 Sigma: 99.1, Range: [1.0, 16.0], Default: 8.0" - ) - i1 = UniformIntegerHyperparameter("param", 0, 100) - assert repr(i1) == "param, Type: UniformInteger, Range: [0, 100], Default: 50" - i2 = NormalIntegerHyperparameter("param", 5, 8) - assert repr(i2) == "param, Type: NormalInteger, Mu: 5 Sigma: 8, Default: 5" - i3 = NormalIntegerHyperparameter("param", 5, 8, lower=1, upper=10) - assert repr(i3) == "param, Type: NormalInteger, Mu: 5 Sigma: 8, Range: [1, 10], Default: 5" - o1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) - assert ( - repr(o1) - == "temp, Type: Ordinal, Sequence: {freezing, cold, warm, hot}, Default: freezing" - ) - c1 = CategoricalHyperparameter("param", [True, False]) - assert repr(c1) == "param, Type: Categorical, Choices: {True, False}, Default: True" + +def test_log_space_conversion(): + lower, upper = 1e-5, 1e5 + hyper = UniformFloatHyperparameter("test", lower=lower, upper=upper, log=True) + assert hyper.is_legal(hyper._transform(1.0)) + + lower, upper = 1e-10, 1e10 + hyper = UniformFloatHyperparameter("test", lower=lower, upper=upper, log=True) + assert hyper.is_legal(hyper._transform(1.0)) + + +def test_ordinal_attributes_accessible(): + f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) + assert f1.name == "temp" + assert f1.sequence == ("freezing", "cold", "warm", "hot") + assert f1.num_elements == 4 + assert f1.default_value == "freezing" + assert f1.normalized_default_value == 0 + + +def test_ordinal_is_legal(): + f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) + assert f1.is_legal("warm") + assert f1.is_legal("freezing") + assert not f1.is_legal("chill") + assert not f1.is_legal(2.5) + assert not f1.is_legal("3") + + # Test is legal vector + assert f1.is_legal_vector(1.0) + assert f1.is_legal_vector(0.0) + assert f1.is_legal_vector(0) + assert f1.is_legal_vector(3) + assert not f1.is_legal_vector(-0.1) + with pytest.raises(TypeError): + f1.is_legal_vector("Hahaha") + + +def test_ordinal_check_order(): + f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) + assert f1.check_order("freezing", "cold") + assert f1.check_order("freezing", "hot") + assert not f1.check_order("hot", "cold") + assert not f1.check_order("hot", "warm") + + +def test_ordinal_get_value(): + f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) + assert f1.get_value(3) == "hot" + assert f1.get_value(1) != "warm" + + +def test_ordinal_get_order(): + f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) + assert f1.get_order("warm") == 2 + assert f1.get_order("freezing") != 3 + + +def test_ordinal_get_seq_order(): + f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) + assert tuple(f1.get_seq_order()) == (0, 1, 2, 3) + + +def test_ordinal_get_neighbors(): + f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) + assert f1.get_neighbors(0, rs=None) == [1] + assert f1.get_neighbors(1, rs=None) == [0, 2] + assert f1.get_neighbors(3, rs=None) == [2] + assert f1.get_neighbors("hot", transform=True, rs=None) == ["warm"] + assert f1.get_neighbors("cold", transform=True, rs=None) == ["freezing", "warm"] + + +def test_get_num_neighbors(): + f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) + assert f1.get_num_neighbors("freezing") == 1 + assert f1.get_num_neighbors("hot") == 1 + assert f1.get_num_neighbors("cold") == 2 + + +def test_ordinal_get_size(): + f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) + assert f1.get_size() == 4 + + +def test_ordinal_pdf(): + c1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) + point_1 = np.array(["freezing"]) + point_2 = np.array(["warm"]) + array_1 = np.array(["freezing", "warm"]) + + wrong_shape_1 = np.array([["freezing"]]) + wrong_shape_2 = np.array(["freezing", "warm"]).reshape(1, -1) + wrong_shape_3 = np.array(["freezing", "warm"]).reshape(-1, 1) + + assert c1.pdf(point_1)[0] == 0.25 + assert c1.pdf(point_2)[0] == 0.25 + + array_results = c1.pdf(array_1) + expected_results = np.array([0.25, 0.25]) + assert array_results.shape == expected_results.shape + for res, exp_res in zip(array_results, expected_results): + assert res == exp_res + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1.pdf(0.2) + with pytest.raises(TypeError): + c1.pdf("pdf") + with pytest.raises(TypeError): + c1.pdf("one") + with pytest.raises(ValueError): + c1.pdf(np.array(["zero"])) + + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_1) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_2) + with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): + c1.pdf(wrong_shape_3) + + +def test_ordinal__pdf(): + c1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) + point_1 = np.array(["freezing"]) + point_2 = np.array(["warm"]) + array_1 = np.array(["freezing", "warm"]) + assert c1._pdf(point_1)[0] == 0.25 + assert c1._pdf(point_2)[0] == 0.25 + + array_results = c1._pdf(array_1) + expected_results = np.array([0.25, 0.25]) + assert array_results.shape == expected_results.shape + for res, exp_res in zip(array_results, expected_results): + assert res == exp_res + + # pdf must take a numpy array + with pytest.raises(TypeError): + c1._pdf(0.2) + with pytest.raises(TypeError): + c1._pdf("pdf") + with pytest.raises(TypeError): + c1._pdf("one") + with pytest.raises(ValueError): + c1._pdf(np.array(["zero"])) + + +def test_ordinal_get_max_density(): + c1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) + c2 = OrdinalHyperparameter("temp", ["freezing", "cold"]) + assert c1.get_max_density() == 0.25 + assert c2.get_max_density() == 0.5 + + +def test_rvs(): + f1 = UniformFloatHyperparameter("param", 0, 10) + + # test that returned types are correct + # if size=None, return a value, but if size=1, return a 1-element array + assert isinstance(f1.rvs(), float) + assert isinstance(f1.rvs(size=1), np.ndarray) + assert isinstance(f1.rvs(size=2), np.ndarray) + + assert f1.rvs(random_state=100) == pytest.approx(f1.rvs(random_state=100)) + assert f1.rvs(random_state=100) == pytest.approx(f1.rvs(random_state=np.random.RandomState(100))) + f1.rvs(random_state=np.random) + f1.rvs(random_state=np.random.default_rng(1)) + with pytest.raises(ValueError): + f1.rvs(1, "a") + + +def test_hyperparam_representation(): + # Float + f1 = UniformFloatHyperparameter("param", 1, 100, log=True) + assert repr(f1) == "param, Type: UniformFloat, Range: [1.0, 100.0], Default: 10.0, on log-scale" + f2 = NormalFloatHyperparameter("param", 8, 99.1, log=False) + assert repr(f2) == "param, Type: NormalFloat, Mu: 8.0 Sigma: 99.1, Default: 8.0" + f3 = NormalFloatHyperparameter("param", 8, 99.1, log=False, lower=1, upper=16) + assert ( + repr(f3) + == "param, Type: NormalFloat, Mu: 8.0 Sigma: 99.1, Range: [1.0, 16.0], Default: 8.0" + ) + i1 = UniformIntegerHyperparameter("param", 0, 100) + assert repr(i1) == "param, Type: UniformInteger, Range: [0, 100], Default: 50" + i2 = NormalIntegerHyperparameter("param", 5, 8) + assert repr(i2) == "param, Type: NormalInteger, Mu: 5 Sigma: 8, Default: 5" + i3 = NormalIntegerHyperparameter("param", 5, 8, lower=1, upper=10) + assert repr(i3) == "param, Type: NormalInteger, Mu: 5 Sigma: 8, Range: [1, 10], Default: 5" + o1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) + assert ( + repr(o1) == "temp, Type: Ordinal, Sequence: {freezing, cold, warm, hot}, Default: freezing" + ) + c1 = CategoricalHyperparameter("param", [True, False]) + assert repr(c1) == "param, Type: Categorical, Choices: {True, False}, Default: True" diff --git a/test/test_util.py b/test/test_util.py index 3cd6bfdc..8d7379aa 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -28,9 +28,9 @@ from __future__ import annotations import os -import unittest import numpy as np +import pytest from pytest import approx import ConfigSpace.c_util @@ -61,567 +61,576 @@ ) -class UtilTest(unittest.TestCase): - def test_impute_inactive_values(self): - mini_autosklearn_config_space_path = os.path.join( - os.path.dirname(__file__), - "test_searchspaces", - "mini_autosklearn_original.pcs", - ) - with open(mini_autosklearn_config_space_path) as fh: - cs = read(fh) - - cs.seed(1) - configuration = cs.sample_configuration() - new_configuration = impute_inactive_values(configuration) - assert id(configuration) != id(new_configuration) - assert len(new_configuration) == 11 - for key in new_configuration: - assert new_configuration[key] is not None - assert new_configuration["random_forest:max_features"] == 9 - - def _test_random_neigbor(self, hp): - cs = ConfigurationSpace() - if not isinstance(hp, list): - hp = [hp] - for hp_ in hp: - cs.add_hyperparameter(hp_) - cs.seed(1) - config = cs.sample_configuration() - for i in range(100): - new_config = get_random_neighbor(config, i) +def _test_random_neigbor(hp): + cs = ConfigurationSpace() + if not isinstance(hp, list): + hp = [hp] + for hp_ in hp: + cs.add_hyperparameter(hp_) + cs.seed(1) + config = cs.sample_configuration() + for i in range(100): + new_config = get_random_neighbor(config, i) + assert config != new_config + + +def _test_get_one_exchange_neighbourhood(hp): + cs = ConfigurationSpace() + num_neighbors = 0 + if not isinstance(hp, list): + hp = [hp] + for hp_ in hp: + cs.add_hyperparameter(hp_) + if np.isinf(hp_.get_num_neighbors()): + num_neighbors += 4 + else: + num_neighbors += hp_.get_num_neighbors() + + cs.seed(1) + config = cs.get_default_configuration() + all_neighbors = [] + for i in range(100): + neighborhood = get_one_exchange_neighbourhood(config, i) + for new_config in neighborhood: assert config != new_config - - def _test_get_one_exchange_neighbourhood(self, hp): - cs = ConfigurationSpace() - num_neighbors = 0 - if not isinstance(hp, list): - hp = [hp] - for hp_ in hp: - cs.add_hyperparameter(hp_) - if np.isinf(hp_.get_num_neighbors()): - num_neighbors += 4 - else: - num_neighbors += hp_.get_num_neighbors() - - cs.seed(1) - config = cs.get_default_configuration() - all_neighbors = [] - for i in range(100): - neighborhood = get_one_exchange_neighbourhood(config, i) - for new_config in neighborhood: - assert config != new_config - assert dict(config) != dict(new_config) - all_neighbors.append(new_config) - - return all_neighbors - - def test_random_neighbor_float(self): - hp = UniformFloatHyperparameter("a", 1, 10) - self._test_random_neigbor(hp) - hp = UniformFloatHyperparameter("a", 1, 10, log=True) - self._test_random_neigbor(hp) - - def test_random_neighborhood_float(self): - hp = UniformFloatHyperparameter("a", 1, 10) - all_neighbors = self._test_get_one_exchange_neighbourhood(hp) - all_neighbors = [neighbor["a"] for neighbor in all_neighbors] - self.assertAlmostEqual(5.49, np.mean(all_neighbors), places=2) - self.assertAlmostEqual(3.192, np.var(all_neighbors), places=2) - hp = UniformFloatHyperparameter("a", 1, 10, log=True) - all_neighbors = self._test_get_one_exchange_neighbourhood(hp) - all_neighbors = [neighbor["a"] for neighbor in all_neighbors] - # Default value is 3.16 - self.assertAlmostEqual(3.50, np.mean(all_neighbors), places=2) - self.assertAlmostEqual(2.79, np.var(all_neighbors), places=2) - - def test_random_neighbor_int(self): - hp = UniformIntegerHyperparameter("a", 1, 10) - self._test_random_neigbor(hp) - hp = UniformIntegerHyperparameter("a", 1, 10, log=True) - self._test_random_neigbor(hp) - - def test_random_neighborhood_int(self): - hp = UniformIntegerHyperparameter("a", 1, 10) - all_neighbors = self._test_get_one_exchange_neighbourhood(hp) - all_neighbors = [neighbor["a"] for neighbor in all_neighbors] - self.assertAlmostEqual(5.8125, np.mean(all_neighbors), places=2) - self.assertAlmostEqual(5.6023, np.var(all_neighbors), places=2) - - hp = UniformIntegerHyperparameter("a", 1, 10, log=True) - all_neighbors = self._test_get_one_exchange_neighbourhood(hp) - all_neighbors = [neighbor["a"] for neighbor in all_neighbors] - # Default value is 3.16 - self.assertAlmostEqual(3.9375, np.mean(all_neighbors), places=2) - self.assertAlmostEqual(5.8886, np.var(all_neighbors), places=2) - - cs = ConfigurationSpace() - cs.add_hyperparameter(hp) - for val in range(1, 11): - config = Configuration(cs, values={"a": val}) - for _i in range(100): - neighborhood = get_one_exchange_neighbourhood(config, 1) - neighbors = [neighbor["a"] for neighbor in neighborhood] - assert len(neighbors) == len(np.unique(neighbors)), neighbors - assert val not in neighbors, neighbors - - def test_random_neighbor_cat(self): - hp = CategoricalHyperparameter("a", [5, 6, 7, 8]) - all_neighbors = self._test_get_one_exchange_neighbourhood(hp) - all_neighbors = list(all_neighbors) - assert len(all_neighbors) == 300 # 3 (neighbors) * 100 (samples) - - def test_random_neighborhood_cat(self): - hp = CategoricalHyperparameter("a", [5, 6, 7, 8]) - self._test_random_neigbor(hp) - - def test_random_neighbor_failing(self): - hp = Constant("a", "b") - self.assertRaisesRegex( - ValueError, - "Probably caught in an infinite " "loop.", - self._test_random_neigbor, - hp, - ) - - hp = CategoricalHyperparameter("a", ["a"]) - self.assertRaisesRegex( - ValueError, - "Probably caught in an infinite " "loop.", - self._test_random_neigbor, - hp, - ) - - def test_random_neigbor_conditional(self): - mini_autosklearn_config_space_path = os.path.join( - os.path.dirname(__file__), - "test_searchspaces", - "mini_autosklearn_original.pcs", - ) - with open(mini_autosklearn_config_space_path) as fh: - cs = read(fh) - - cs.seed(1) - configuration = cs.get_default_configuration() - for i in range(100): - new_config = get_random_neighbor(configuration, i) + assert dict(config) != dict(new_config) + all_neighbors.append(new_config) + + return all_neighbors + + +def test_impute_inactive_values(): + mini_autosklearn_config_space_path = os.path.join( + os.path.dirname(__file__), + "test_searchspaces", + "mini_autosklearn_original.pcs", + ) + with open(mini_autosklearn_config_space_path) as fh: + cs = read(fh) + + cs.seed(1) + configuration = cs.sample_configuration() + new_configuration = impute_inactive_values(configuration) + assert id(configuration) != id(new_configuration) + assert len(new_configuration) == 11 + for key in new_configuration: + assert new_configuration[key] is not None + assert new_configuration["random_forest:max_features"] == 9 + + +def test_random_neighbor_float(): + hp = UniformFloatHyperparameter("a", 1, 10) + _test_random_neigbor(hp) + hp = UniformFloatHyperparameter("a", 1, 10, log=True) + _test_random_neigbor(hp) + + +def test_random_neighborhood_float(): + hp = UniformFloatHyperparameter("a", 1, 10) + all_neighbors = _test_get_one_exchange_neighbourhood(hp) + all_neighbors = [neighbor["a"] for neighbor in all_neighbors] + assert pytest.approx(np.mean(all_neighbors), abs=1e-2) == 5.49 + assert pytest.approx(np.var(all_neighbors), abs=1e-2) == 3.192 + hp = UniformFloatHyperparameter("a", 1, 10, log=True) + all_neighbors = _test_get_one_exchange_neighbourhood(hp) + all_neighbors = [neighbor["a"] for neighbor in all_neighbors] + # Default value is 3.16 + assert pytest.approx(np.mean(all_neighbors), abs=1e-2) == 3.50 + assert pytest.approx(np.var(all_neighbors), abs=1e-2) == 2.79 + + +def test_random_neighbor_int(): + hp = UniformIntegerHyperparameter("a", 1, 10) + _test_random_neigbor(hp) + hp = UniformIntegerHyperparameter("a", 1, 10, log=True) + _test_random_neigbor(hp) + + +def test_random_neighborhood_int(): + hp = UniformIntegerHyperparameter("a", 1, 10) + all_neighbors = _test_get_one_exchange_neighbourhood(hp) + all_neighbors = [neighbor["a"] for neighbor in all_neighbors] + assert pytest.approx(np.mean(all_neighbors), abs=1e-2) == 5.8125 + assert pytest.approx(np.var(all_neighbors), abs=1e-2) == 5.6023 + + hp = UniformIntegerHyperparameter("a", 1, 10, log=True) + all_neighbors = _test_get_one_exchange_neighbourhood(hp) + all_neighbors = [neighbor["a"] for neighbor in all_neighbors] + # Default value is 3.16 + assert pytest.approx(np.mean(all_neighbors), abs=1e-2) == 3.9375 + assert pytest.approx(np.var(all_neighbors), abs=1e-2) == 5.8886 + + cs = ConfigurationSpace() + cs.add_hyperparameter(hp) + for val in range(1, 11): + config = Configuration(cs, values={"a": val}) + for _ in range(100): + neighborhood = get_one_exchange_neighbourhood(config, 1) + neighbors = [neighbor["a"] for neighbor in neighborhood] + assert len(neighbors) == len(np.unique(neighbors)), neighbors + assert val not in neighbors, neighbors + + +def test_random_neighbor_cat(): + hp = CategoricalHyperparameter("a", [5, 6, 7, 8]) + all_neighbors = _test_get_one_exchange_neighbourhood(hp) + all_neighbors = list(all_neighbors) + assert len(all_neighbors) == 300 # 3 (neighbors) * 100 (samples) + + +def test_random_neighborhood_cat(): + hp = CategoricalHyperparameter("a", [5, 6, 7, 8]) + _test_random_neigbor(hp) + + +def test_random_neighbor_failing(): + hp = Constant("a", "b") + with pytest.raises(ValueError, match="Probably caught in an infinite " "loop."): + _test_random_neigbor(hp) + + hp = CategoricalHyperparameter("a", ["a"]) + with pytest.raises(ValueError, match="Probably caught in an infinite " "loop."): + _test_random_neigbor(hp) + + +def test_random_neigbor_conditional(): + mini_autosklearn_config_space_path = os.path.join( + os.path.dirname(__file__), + "test_searchspaces", + "mini_autosklearn_original.pcs", + ) + with open(mini_autosklearn_config_space_path) as fh: + cs = read(fh) + + cs.seed(1) + configuration = cs.get_default_configuration() + for i in range(100): + new_config = get_random_neighbor(configuration, i) + assert configuration != new_config + + +def test_random_neigborhood_conditional(): + mini_autosklearn_config_space_path = os.path.join( + os.path.dirname(__file__), + "test_searchspaces", + "mini_autosklearn_original.pcs", + ) + with open(mini_autosklearn_config_space_path) as fh: + cs = read(fh) + + cs.seed(1) + configuration = cs.get_default_configuration() + for i in range(100): + neighborhood = get_one_exchange_neighbourhood(configuration, i) + for new_config in neighborhood: assert configuration != new_config - def test_random_neigborhood_conditional(self): - mini_autosklearn_config_space_path = os.path.join( - os.path.dirname(__file__), - "test_searchspaces", - "mini_autosklearn_original.pcs", - ) - with open(mini_autosklearn_config_space_path) as fh: - cs = read(fh) - - cs.seed(1) - configuration = cs.get_default_configuration() - for i in range(100): - neighborhood = get_one_exchange_neighbourhood(configuration, i) - for new_config in neighborhood: - assert configuration != new_config - - def test_deactivate_inactive_hyperparameters(self): - diamond = ConfigurationSpace() - head = CategoricalHyperparameter("head", [0, 1]) - left = CategoricalHyperparameter("left", [0, 1]) - right = CategoricalHyperparameter("right", [0, 1]) - bottom = CategoricalHyperparameter("bottom", [0, 1]) - diamond.add_hyperparameters([head, left, right, bottom]) - diamond.add_condition(EqualsCondition(left, head, 0)) - diamond.add_condition(EqualsCondition(right, head, 0)) - diamond.add_condition( - AndConjunction(EqualsCondition(bottom, left, 0), EqualsCondition(bottom, right, 0)), - ) - - c = deactivate_inactive_hyperparameters( - {"head": 0, "left": 0, "right": 0, "bottom": 0}, - diamond, - ) - diamond._check_configuration_rigorous(c) - c = deactivate_inactive_hyperparameters( - {"head": 1, "left": 0, "right": 0, "bottom": 0}, - diamond, - ) - diamond._check_configuration_rigorous(c) - - c = deactivate_inactive_hyperparameters( - {"head": 0, "left": 1, "right": 0, "bottom": 0}, - diamond, - ) - diamond._check_configuration_rigorous(c) - - diamond = ConfigurationSpace() - head = CategoricalHyperparameter("head", [0, 1]) - left = CategoricalHyperparameter("left", [0, 1]) - right = CategoricalHyperparameter("right", [0, 1]) - bottom = CategoricalHyperparameter("bottom", [0, 1]) - diamond.add_hyperparameters([head, left, right, bottom]) - diamond.add_condition(EqualsCondition(left, head, 0)) - diamond.add_condition(EqualsCondition(right, head, 0)) - diamond.add_condition( - OrConjunction(EqualsCondition(bottom, left, 0), EqualsCondition(bottom, right, 0)), - ) - - c = deactivate_inactive_hyperparameters( - {"head": 0, "left": 0, "right": 0, "bottom": 0}, - diamond, - ) - diamond._check_configuration_rigorous(c) - - c = deactivate_inactive_hyperparameters( - {"head": 1, "left": 1, "right": 0, "bottom": 0}, - diamond, - ) - diamond._check_configuration_rigorous(c) - - c = deactivate_inactive_hyperparameters( - {"head": 0, "left": 1, "right": 0, "bottom": 0}, - diamond, - ) - diamond._check_configuration_rigorous(c) - - plain = ConfigurationSpace() - a = UniformIntegerHyperparameter("a", 0, 10) - b = UniformIntegerHyperparameter("b", 0, 10) - plain.add_hyperparameters([a, b]) - c = deactivate_inactive_hyperparameters({"a": 5, "b": 6}, plain) - plain.check_configuration(c) - - def test_check_neighbouring_config_diamond(self): - diamond = ConfigurationSpace() - head = CategoricalHyperparameter("head", [0, 1]) - left = CategoricalHyperparameter("left", [0, 1]) - right = CategoricalHyperparameter("right", [0, 1, 2, 3]) - bottom = CategoricalHyperparameter("bottom", [0, 1]) - diamond.add_hyperparameters([head, left, right, bottom]) - diamond.add_condition(EqualsCondition(left, head, 0)) - diamond.add_condition(EqualsCondition(right, head, 0)) - diamond.add_condition( - AndConjunction(EqualsCondition(bottom, left, 1), EqualsCondition(bottom, right, 1)), - ) - - config = Configuration(diamond, {"bottom": 0, "head": 0, "left": 1, "right": 1}) - hp_name = "head" - index = diamond.get_idx_by_hyperparameter_name(hp_name) - neighbor_value = 1 - - new_array = ConfigSpace.c_util.change_hp_value( - diamond, - config.get_array(), - hp_name, - neighbor_value, - index, - ) - expected_array = np.array([1, np.nan, np.nan, np.nan]) - - np.testing.assert_almost_equal(new_array, expected_array) - - def test_check_neighbouring_config_diamond_or_conjunction(self): - diamond = ConfigurationSpace() - top = CategoricalHyperparameter("top", [0, 1], 0) - middle = CategoricalHyperparameter("middle", [0, 1], 1) - bottom_left = CategoricalHyperparameter("bottom_left", [0, 1], 1) - bottom_right = CategoricalHyperparameter("bottom_right", [0, 1, 2, 3], 1) - - diamond.add_hyperparameters([top, bottom_left, bottom_right, middle]) - diamond.add_condition(EqualsCondition(middle, top, 0)) - diamond.add_condition(EqualsCondition(bottom_left, middle, 0)) - diamond.add_condition( - OrConjunction( - EqualsCondition(bottom_right, middle, 1), - EqualsCondition(bottom_right, top, 1), - ), - ) - - config = Configuration(diamond, {"top": 0, "middle": 1, "bottom_right": 1}) - hp_name = "top" - index = diamond.get_idx_by_hyperparameter_name(hp_name) - neighbor_value = 1 - - new_array = ConfigSpace.c_util.change_hp_value( - diamond, - config.get_array(), - hp_name, - neighbor_value, - index, - ) - expected_array = np.array([1, np.nan, np.nan, 1]) - - np.testing.assert_almost_equal(new_array, expected_array) - - def test_check_neighbouring_config_diamond_str(self): - diamond = ConfigurationSpace() - head = CategoricalHyperparameter("head", ["red", "green"]) - left = CategoricalHyperparameter("left", ["red", "green"]) - right = CategoricalHyperparameter("right", ["red", "green", "blue", "yellow"]) - bottom = CategoricalHyperparameter("bottom", ["red", "green"]) - diamond.add_hyperparameters([head, left, right, bottom]) - diamond.add_condition(EqualsCondition(left, head, "red")) - diamond.add_condition(EqualsCondition(right, head, "red")) - diamond.add_condition( - AndConjunction( - EqualsCondition(bottom, left, "green"), - EqualsCondition(bottom, right, "green"), - ), - ) - - config = Configuration( - diamond, - {"bottom": "red", "head": "red", "left": "green", "right": "green"}, - ) - hp_name = "head" - index = diamond.get_idx_by_hyperparameter_name(hp_name) - neighbor_value = 1 - - new_array = ConfigSpace.c_util.change_hp_value( - diamond, - config.get_array(), - hp_name, - neighbor_value, - index, +def test_deactivate_inactive_hyperparameters(): + diamond = ConfigurationSpace() + head = CategoricalHyperparameter("head", [0, 1]) + left = CategoricalHyperparameter("left", [0, 1]) + right = CategoricalHyperparameter("right", [0, 1]) + bottom = CategoricalHyperparameter("bottom", [0, 1]) + diamond.add_hyperparameters([head, left, right, bottom]) + diamond.add_condition(EqualsCondition(left, head, 0)) + diamond.add_condition(EqualsCondition(right, head, 0)) + diamond.add_condition( + AndConjunction(EqualsCondition(bottom, left, 0), EqualsCondition(bottom, right, 0)), + ) + + c = deactivate_inactive_hyperparameters( + {"head": 0, "left": 0, "right": 0, "bottom": 0}, + diamond, + ) + diamond._check_configuration_rigorous(c) + + c = deactivate_inactive_hyperparameters( + {"head": 1, "left": 0, "right": 0, "bottom": 0}, + diamond, + ) + diamond._check_configuration_rigorous(c) + + c = deactivate_inactive_hyperparameters( + {"head": 0, "left": 1, "right": 0, "bottom": 0}, + diamond, + ) + diamond._check_configuration_rigorous(c) + + diamond = ConfigurationSpace() + head = CategoricalHyperparameter("head", [0, 1]) + left = CategoricalHyperparameter("left", [0, 1]) + right = CategoricalHyperparameter("right", [0, 1]) + bottom = CategoricalHyperparameter("bottom", [0, 1]) + diamond.add_hyperparameters([head, left, right, bottom]) + diamond.add_condition(EqualsCondition(left, head, 0)) + diamond.add_condition(EqualsCondition(right, head, 0)) + diamond.add_condition( + OrConjunction(EqualsCondition(bottom, left, 0), EqualsCondition(bottom, right, 0)), + ) + + c = deactivate_inactive_hyperparameters( + {"head": 0, "left": 0, "right": 0, "bottom": 0}, + diamond, + ) + diamond._check_configuration_rigorous(c) + + c = deactivate_inactive_hyperparameters( + {"head": 1, "left": 1, "right": 0, "bottom": 0}, + diamond, + ) + diamond._check_configuration_rigorous(c) + + c = deactivate_inactive_hyperparameters( + {"head": 0, "left": 1, "right": 0, "bottom": 0}, + diamond, + ) + diamond._check_configuration_rigorous(c) + + plain = ConfigurationSpace() + a = UniformIntegerHyperparameter("a", 0, 10) + b = UniformIntegerHyperparameter("b", 0, 10) + plain.add_hyperparameters([a, b]) + c = deactivate_inactive_hyperparameters({"a": 5, "b": 6}, plain) + plain.check_configuration(c) + + +def test_check_neighbouring_config_diamond(): + diamond = ConfigurationSpace() + head = CategoricalHyperparameter("head", [0, 1]) + left = CategoricalHyperparameter("left", [0, 1]) + right = CategoricalHyperparameter("right", [0, 1, 2, 3]) + bottom = CategoricalHyperparameter("bottom", [0, 1]) + diamond.add_hyperparameters([head, left, right, bottom]) + diamond.add_condition(EqualsCondition(left, head, 0)) + diamond.add_condition(EqualsCondition(right, head, 0)) + diamond.add_condition( + AndConjunction(EqualsCondition(bottom, left, 1), EqualsCondition(bottom, right, 1)), + ) + + config = Configuration(diamond, {"bottom": 0, "head": 0, "left": 1, "right": 1}) + hp_name = "head" + index = diamond.get_idx_by_hyperparameter_name(hp_name) + neighbor_value = 1 + + new_array = ConfigSpace.c_util.change_hp_value( + diamond, + config.get_array(), + hp_name, + neighbor_value, + index, + ) + expected_array = np.array([1, np.nan, np.nan, np.nan]) + + np.testing.assert_almost_equal(new_array, expected_array) + + +def test_check_neighbouring_config_diamond_or_conjunction(): + diamond = ConfigurationSpace() + top = CategoricalHyperparameter("top", [0, 1], 0) + middle = CategoricalHyperparameter("middle", [0, 1], 1) + bottom_left = CategoricalHyperparameter("bottom_left", [0, 1], 1) + bottom_right = CategoricalHyperparameter("bottom_right", [0, 1, 2, 3], 1) + + diamond.add_hyperparameters([top, bottom_left, bottom_right, middle]) + diamond.add_condition(EqualsCondition(middle, top, 0)) + diamond.add_condition(EqualsCondition(bottom_left, middle, 0)) + diamond.add_condition( + OrConjunction( + EqualsCondition(bottom_right, middle, 1), + EqualsCondition(bottom_right, top, 1), + ), + ) + + config = Configuration(diamond, {"top": 0, "middle": 1, "bottom_right": 1}) + hp_name = "top" + index = diamond.get_idx_by_hyperparameter_name(hp_name) + neighbor_value = 1 + + new_array = ConfigSpace.c_util.change_hp_value( + diamond, + config.get_array(), + hp_name, + neighbor_value, + index, + ) + expected_array = np.array([1, np.nan, np.nan, 1]) + + np.testing.assert_almost_equal(new_array, expected_array) + + +def test_check_neighbouring_config_diamond_str(): + diamond = ConfigurationSpace() + head = CategoricalHyperparameter("head", ["red", "green"]) + left = CategoricalHyperparameter("left", ["red", "green"]) + right = CategoricalHyperparameter("right", ["red", "green", "blue", "yellow"]) + bottom = CategoricalHyperparameter("bottom", ["red", "green"]) + diamond.add_hyperparameters([head, left, right, bottom]) + diamond.add_condition(EqualsCondition(left, head, "red")) + diamond.add_condition(EqualsCondition(right, head, "red")) + diamond.add_condition( + AndConjunction( + EqualsCondition(bottom, left, "green"), + EqualsCondition(bottom, right, "green"), + ), + ) + + config = Configuration( + diamond, + {"bottom": "red", "head": "red", "left": "green", "right": "green"}, + ) + hp_name = "head" + index = diamond.get_idx_by_hyperparameter_name(hp_name) + neighbor_value = 1 + + new_array = ConfigSpace.c_util.change_hp_value( + diamond, + config.get_array(), + hp_name, + neighbor_value, + index, + ) + expected_array = np.array([1, np.nan, np.nan, np.nan]) + + np.testing.assert_almost_equal(new_array, expected_array) + + +def test_fix_types(): + # Test categorical and ordinal + for hyperparameter_type in [CategoricalHyperparameter, OrdinalHyperparameter]: + cs = ConfigurationSpace() + cs.add_hyperparameters( + [ + hyperparameter_type("bools", [True, False]), + hyperparameter_type("ints", [1, 2, 3, 4, 5]), + hyperparameter_type("floats", [1.5, 2.5, 3.5, 4.5, 5.5]), + hyperparameter_type("str", ["string", "ding", "dong"]), + hyperparameter_type("mixed", [2, True, 1.5, "string", False, "False"]), + ], ) - expected_array = np.array([1, np.nan, np.nan, np.nan]) - - np.testing.assert_almost_equal(new_array, expected_array) - - def test_fix_types(self): - # Test categorical and ordinal - for hyperparameter_type in [CategoricalHyperparameter, OrdinalHyperparameter]: - cs = ConfigurationSpace() - cs.add_hyperparameters( - [ - hyperparameter_type("bools", [True, False]), - hyperparameter_type("ints", [1, 2, 3, 4, 5]), - hyperparameter_type("floats", [1.5, 2.5, 3.5, 4.5, 5.5]), - hyperparameter_type("str", ["string", "ding", "dong"]), - hyperparameter_type("mixed", [2, True, 1.5, "string", False, "False"]), - ], - ) - c = dict(cs.get_default_configuration()) - # Check bools - for b in [False, True]: - c["bools"] = b - c_str = {k: str(v) for k, v in c.items()} - assert fix_types(c_str, cs) == c - # Check legal mixed values - for m in [2, True, 1.5, "string"]: - c["mixed"] = m - c_str = {k: str(v) for k, v in c.items()} - assert fix_types(c_str, cs) == c - # Check error on cornercase that cannot be caught - for m in [False, "False"]: - c["mixed"] = m - c_str = {k: str(v) for k, v in c.items()} - self.assertRaises(ValueError, fix_types, c_str, cs) - # Test constant - for m in [2, 1.5, "string"]: - cs = ConfigurationSpace() - cs.add_hyperparameter(Constant("constant", m)) - c = dict(cs.get_default_configuration()) + c = dict(cs.get_default_configuration()) + # Check bools + for b in [False, True]: + c["bools"] = b c_str = {k: str(v) for k, v in c.items()} assert fix_types(c_str, cs) == c - - def test_generate_grid(self): - """Test grid generation.""" - # Sub-test 1 - cs = ConfigurationSpace(seed=1234) - - cat1 = CategoricalHyperparameter(name="cat1", choices=["T", "F"]) - const1 = Constant(name="const1", value=4) - float1 = UniformFloatHyperparameter(name="float1", lower=-1, upper=1, log=False) - int1 = UniformIntegerHyperparameter(name="int1", lower=10, upper=100, log=True) - ord1 = OrdinalHyperparameter(name="ord1", sequence=["1", "2", "3"]) - - cs.add_hyperparameters([float1, int1, cat1, ord1, const1]) - - num_steps_dict = {"float1": 11, "int1": 6} - generated_grid = generate_grid(cs, num_steps_dict) - - # Check randomly pre-selected values in the generated_grid - # 2 * 1 * 11 * 6 * 3 total diff. possible configurations - assert len(generated_grid) == 396 - # Check 1st and last generated configurations completely: - first_expected_dict = { - "cat1": "T", - "const1": 4, - "float1": -1.0, - "int1": 10, - "ord1": "1", - } - last_expected_dict = { - "cat1": "F", - "const1": 4, - "float1": 1.0, - "int1": 100, - "ord1": "3", - } - assert dict(generated_grid[0]) == first_expected_dict - assert dict(generated_grid[-1]) == last_expected_dict - assert generated_grid[198]["cat1"] == "F" - assert generated_grid[45]["const1"] == 4 - # The 2 most frequently changing HPs (int1 and ord1) have 3 * 6 = 18 different values for - # each value of float1, so the 4th value of float1 of -0.4 is reached after - # 3 * 18 = 54 values in the generated_grid (and remains the same for the next 18 values): - for i in range(18): - self.assertAlmostEqual(generated_grid[54 + i]["float1"], -0.4, places=2) - # 5th diff. value for int1 after 4 * 3 = 12 values. Reasoning as above. - assert generated_grid[12]["int1"] == 63 - assert generated_grid[3]["ord1"] == "1" - assert generated_grid[4]["ord1"] == "2" - assert generated_grid[5]["ord1"] == "3" - - # Sub-test 2 - # Test for extreme cases: only numerical - cs = ConfigurationSpace(seed=1234) - cs.add_hyperparameters([float1, int1]) - - num_steps_dict = {"float1": 11, "int1": 6} - generated_grid = generate_grid(cs, num_steps_dict) - - assert len(generated_grid) == 66 - # Check 1st and last generated configurations completely: - first_expected_dict = {"float1": -1.0, "int1": 10} - last_expected_dict = {"float1": 1.0, "int1": 100} - assert dict(generated_grid[0]) == first_expected_dict - assert dict(generated_grid[-1]) == last_expected_dict - - # Test: only categorical - cs = ConfigurationSpace(seed=1234) - cs.add_hyperparameters([cat1]) - - generated_grid = generate_grid(cs) - - assert len(generated_grid) == 2 - # Check 1st and last generated configurations completely: - assert generated_grid[0]["cat1"] == "T" - assert generated_grid[-1]["cat1"] == "F" - - # Test: only constant - cs = ConfigurationSpace(seed=1234) - cs.add_hyperparameters([const1]) - + # Check legal mixed values + for m in [2, True, 1.5, "string"]: + c["mixed"] = m + c_str = {k: str(v) for k, v in c.items()} + assert fix_types(c_str, cs) == c + # Check error on cornercase that cannot be caught + for m in [False, "False"]: + c["mixed"] = m + c_str = {k: str(v) for k, v in c.items()} + with pytest.raises(ValueError): + fix_types(c_str, cs) + # Test constant + for m in [2, 1.5, "string"]: + cs = ConfigurationSpace() + cs.add_hyperparameter(Constant("constant", m)) + c = dict(cs.get_default_configuration()) + c_str = {k: str(v) for k, v in c.items()} + assert fix_types(c_str, cs) == c + + +def test_generate_grid(): + """Test grid generation.""" + # Sub-test 1 + cs = ConfigurationSpace(seed=1234) + + cat1 = CategoricalHyperparameter(name="cat1", choices=["T", "F"]) + const1 = Constant(name="const1", value=4) + float1 = UniformFloatHyperparameter(name="float1", lower=-1, upper=1, log=False) + int1 = UniformIntegerHyperparameter(name="int1", lower=10, upper=100, log=True) + ord1 = OrdinalHyperparameter(name="ord1", sequence=["1", "2", "3"]) + + cs.add_hyperparameters([float1, int1, cat1, ord1, const1]) + + num_steps_dict = {"float1": 11, "int1": 6} + generated_grid = generate_grid(cs, num_steps_dict) + + # Check randomly pre-selected values in the generated_grid + # 2 * 1 * 11 * 6 * 3 total diff. possible configurations + assert len(generated_grid) == 396 + # Check 1st and last generated configurations completely: + first_expected_dict = { + "cat1": "T", + "const1": 4, + "float1": -1.0, + "int1": 10, + "ord1": "1", + } + last_expected_dict = { + "cat1": "F", + "const1": 4, + "float1": 1.0, + "int1": 100, + "ord1": "3", + } + assert dict(generated_grid[0]) == first_expected_dict + assert dict(generated_grid[-1]) == last_expected_dict + assert generated_grid[198]["cat1"] == "F" + assert generated_grid[45]["const1"] == 4 + # The 2 most frequently changing HPs (int1 and ord1) have 3 * 6 = 18 different values for + # each value of float1, so the 4th value of float1 of -0.4 is reached after + # 3 * 18 = 54 values in the generated_grid (and remains the same for the next 18 values): + for i in range(18): + assert generated_grid[54 + i]["float1"] == pytest.approx(-0.4, abs=1e-2) + # 5th diff. value for int1 after 4 * 3 = 12 values. Reasoning as above. + assert generated_grid[12]["int1"] == 63 + assert generated_grid[3]["ord1"] == "1" + assert generated_grid[4]["ord1"] == "2" + assert generated_grid[5]["ord1"] == "3" + + # Sub-test 2 + # Test for extreme cases: only numerical + cs = ConfigurationSpace(seed=1234) + cs.add_hyperparameters([float1, int1]) + + num_steps_dict = {"float1": 11, "int1": 6} + generated_grid = generate_grid(cs, num_steps_dict) + + assert len(generated_grid) == 66 + # Check 1st and last generated configurations completely: + first_expected_dict = {"float1": -1.0, "int1": 10} + last_expected_dict = {"float1": 1.0, "int1": 100} + assert dict(generated_grid[0]) == first_expected_dict + assert dict(generated_grid[-1]) == last_expected_dict + + # Test: only categorical + cs = ConfigurationSpace(seed=1234) + cs.add_hyperparameters([cat1]) + + generated_grid = generate_grid(cs) + + assert len(generated_grid) == 2 + # Check 1st and last generated configurations completely: + assert generated_grid[0]["cat1"] == "T" + assert generated_grid[-1]["cat1"] == "F" + + # Test: only constant + cs = ConfigurationSpace(seed=1234) + cs.add_hyperparameters([const1]) + + generated_grid = generate_grid(cs) + + assert len(generated_grid) == 1 + # Check 1st and only generated configuration completely: + assert generated_grid[0]["const1"] == 4 + + # Test: no hyperparameters yet + cs = ConfigurationSpace(seed=1234) + + generated_grid = generate_grid(cs, num_steps_dict) + + # For the case of no hyperparameters, in get_cartesian_product, itertools.product() returns + # a single empty tuple element which leads to a single empty Configuration. + assert len(generated_grid) == 0 + + # Sub-test 3 + # Tests for quantization and conditional spaces. num_steps_dict supports specifying steps + # for only some of the int and float HPs. The rest are taken from the 'q' member variables + # of these HPs. The conditional space tested has 2 levels of conditions. + cs2 = ConfigurationSpace(seed=123) + float1 = UniformFloatHyperparameter(name="float1", lower=-1, upper=1, log=False) + int1 = UniformIntegerHyperparameter(name="int1", lower=0, upper=1000, log=False, q=500) + cs2.add_hyperparameters([float1, int1]) + + int2_cond = UniformIntegerHyperparameter(name="int2_cond", lower=10, upper=100, log=True) + cs2.add_hyperparameters([int2_cond]) + cond_1 = AndConjunction( + LessThanCondition(int2_cond, float1, -0.5), + GreaterThanCondition(int2_cond, int1, 600), + ) + cs2.add_conditions([cond_1]) + cat1_cond = CategoricalHyperparameter(name="cat1_cond", choices=["apple", "orange"]) + cs2.add_hyperparameters([cat1_cond]) + cond_2 = AndConjunction( + GreaterThanCondition(cat1_cond, int1, 300), + LessThanCondition(cat1_cond, int1, 700), + GreaterThanCondition(cat1_cond, float1, -0.5), + LessThanCondition(cat1_cond, float1, 0.5), + ) + cs2.add_conditions([cond_2]) + float2_cond = UniformFloatHyperparameter( + name="float2_cond", + lower=10.0, + upper=100.0, + log=True, + ) + # 2nd level dependency in ConfigurationSpace tree being tested + cs2.add_hyperparameters([float2_cond]) + cond_3 = GreaterThanCondition(float2_cond, int2_cond, 50) + cs2.add_conditions([cond_3]) + num_steps_dict1 = {"float1": 4, "int2_cond": 3, "float2_cond": 3} + generated_grid = generate_grid(cs2, num_steps_dict1) + assert len(generated_grid) == 18 + + # RR: I manually generated the grid and verified the values were correct. + # Check 1st and last generated configurations completely: + first_expected_dict = {"float1": -1.0, "int1": 0} + last_expected_dict = { + "float1": -1.0, + "int1": 1000, + "int2_cond": 100, + "float2_cond": 100.0, + } + + assert dict(generated_grid[0]) == first_expected_dict + + # This was having slight numerical instability (99.99999999999994 vs 100.0) and so + # we manually do a pass over each value + last_config = generated_grid[-1] + for k, expected_value in last_config.items(): + generated_value = last_config[k] + if isinstance(generated_value, float): + assert generated_value == approx(expected_value) + else: + assert generated_value == expected_value + # Here, we test that a few randomly chosen values in the generated grid + # correspond to the ones I checked. + assert generated_grid[3]["int1"] == 1000 + assert generated_grid[12]["cat1_cond"] == "orange" + assert generated_grid[-2]["float2_cond"] == pytest.approx(31.622776601683803, abs=1e-3) + + # Sub-test 4 + # Test: only a single hyperparameter and num_steps_dict is None + cs = ConfigurationSpace(seed=1234) + cs.add_hyperparameters([float1]) + + num_steps_dict = {"float1": 11} + try: generated_grid = generate_grid(cs) - - assert len(generated_grid) == 1 - # Check 1st and only generated configuration completely: - assert generated_grid[0]["const1"] == 4 - - # Test: no hyperparameters yet - cs = ConfigurationSpace(seed=1234) - - generated_grid = generate_grid(cs, num_steps_dict) - - # For the case of no hyperparameters, in get_cartesian_product, itertools.product() returns - # a single empty tuple element which leads to a single empty Configuration. - assert len(generated_grid) == 0 - - # Sub-test 3 - # Tests for quantization and conditional spaces. num_steps_dict supports specifying steps - # for only some of the int and float HPs. The rest are taken from the 'q' member variables - # of these HPs. The conditional space tested has 2 levels of conditions. - cs2 = ConfigurationSpace(seed=123) - float1 = UniformFloatHyperparameter(name="float1", lower=-1, upper=1, log=False) - int1 = UniformIntegerHyperparameter(name="int1", lower=0, upper=1000, log=False, q=500) - cs2.add_hyperparameters([float1, int1]) - - int2_cond = UniformIntegerHyperparameter(name="int2_cond", lower=10, upper=100, log=True) - cs2.add_hyperparameters([int2_cond]) - cond_1 = AndConjunction( - LessThanCondition(int2_cond, float1, -0.5), - GreaterThanCondition(int2_cond, int1, 600), - ) - cs2.add_conditions([cond_1]) - cat1_cond = CategoricalHyperparameter(name="cat1_cond", choices=["apple", "orange"]) - cs2.add_hyperparameters([cat1_cond]) - cond_2 = AndConjunction( - GreaterThanCondition(cat1_cond, int1, 300), - LessThanCondition(cat1_cond, int1, 700), - GreaterThanCondition(cat1_cond, float1, -0.5), - LessThanCondition(cat1_cond, float1, 0.5), + except ValueError as e: + assert ( + str(e) == "num_steps_dict is None or doesn't contain " + "the number of points to divide float1 into. And its quantization " + "factor is None. Please provide/set one of these values." ) - cs2.add_conditions([cond_2]) - float2_cond = UniformFloatHyperparameter( - name="float2_cond", - lower=10.0, - upper=100.0, - log=True, - ) - # 2nd level dependency in ConfigurationSpace tree being tested - cs2.add_hyperparameters([float2_cond]) - cond_3 = GreaterThanCondition(float2_cond, int2_cond, 50) - cs2.add_conditions([cond_3]) - num_steps_dict1 = {"float1": 4, "int2_cond": 3, "float2_cond": 3} - generated_grid = generate_grid(cs2, num_steps_dict1) - assert len(generated_grid) == 18 - - # RR: I manually generated the grid and verified the values were correct. - # Check 1st and last generated configurations completely: - first_expected_dict = {"float1": -1.0, "int1": 0} - last_expected_dict = { - "float1": -1.0, - "int1": 1000, - "int2_cond": 100, - "float2_cond": 100.0, - } - - assert dict(generated_grid[0]) == first_expected_dict - - # This was having slight numerical instability (99.99999999999994 vs 100.0) and so - # we manually do a pass over each value - last_config = generated_grid[-1] - for k, expected_value in last_config.items(): - generated_value = last_config[k] - if isinstance(generated_value, float): - assert generated_value == approx(expected_value) - else: - assert generated_value == expected_value - # Here, we test that a few randomly chosen values in the generated grid - # correspond to the ones I checked. - assert generated_grid[3]["int1"] == 1000 - assert generated_grid[12]["cat1_cond"] == "orange" - self.assertAlmostEqual(generated_grid[-2]["float2_cond"], 31.622776601683803, places=3) - - # Sub-test 4 - # Test: only a single hyperparameter and num_steps_dict is None - cs = ConfigurationSpace(seed=1234) - cs.add_hyperparameters([float1]) - - num_steps_dict = {"float1": 11} - try: - generated_grid = generate_grid(cs) - except ValueError as e: - assert ( - str(e) == "num_steps_dict is None or doesn't contain " - "the number of points to divide float1 into. And its quantization " - "factor is None. Please provide/set one of these values." - ) - - generated_grid = generate_grid(cs, num_steps_dict) - - assert len(generated_grid) == 11 - # Check 1st and last generated configurations completely: - assert generated_grid[0]["float1"] == -1.0 - assert generated_grid[-1]["float1"] == 1.0 - - # Test forbidden clause - cs = ConfigurationSpace(seed=1234) - cs.add_hyperparameters([cat1, ord1, int1]) - cs.add_condition(EqualsCondition(int1, cat1, "T")) # int1 only active if cat1 == T - cs.add_forbidden_clause( - ForbiddenAndConjunction( # Forbid ord1 == 3 if cat1 == F - ForbiddenEqualsClause(cat1, "F"), - ForbiddenEqualsClause(ord1, "3"), - ), - ) - - generated_grid = generate_grid(cs, {"int1": 2}) - assert len(generated_grid) == 8 - assert dict(generated_grid[0]) == {"cat1": "F", "ord1": "1"} - assert dict(generated_grid[1]) == {"cat1": "F", "ord1": "2"} - assert dict(generated_grid[2]) == {"cat1": "T", "ord1": "1", "int1": 0} - assert dict(generated_grid[-1]) == {"cat1": "T", "ord1": "3", "int1": 1000} + generated_grid = generate_grid(cs, num_steps_dict) + + assert len(generated_grid) == 11 + # Check 1st and last generated configurations completely: + assert generated_grid[0]["float1"] == -1.0 + assert generated_grid[-1]["float1"] == 1.0 + + # Test forbidden clause + cs = ConfigurationSpace(seed=1234) + cs.add_hyperparameters([cat1, ord1, int1]) + cs.add_condition(EqualsCondition(int1, cat1, "T")) # int1 only active if cat1 == T + cs.add_forbidden_clause( + ForbiddenAndConjunction( # Forbid ord1 == 3 if cat1 == F + ForbiddenEqualsClause(cat1, "F"), + ForbiddenEqualsClause(ord1, "3"), + ), + ) + + generated_grid = generate_grid(cs, {"int1": 2}) + + assert len(generated_grid) == 8 + assert dict(generated_grid[0]) == {"cat1": "F", "ord1": "1"} + assert dict(generated_grid[1]) == {"cat1": "F", "ord1": "2"} + assert dict(generated_grid[2]) == {"cat1": "T", "ord1": "1", "int1": 0} + assert dict(generated_grid[-1]) == {"cat1": "T", "ord1": "3", "int1": 1000} From e91596473184697fcdb3b466bd182be36703f625 Mon Sep 17 00:00:00 2001 From: eddiebergman Date: Tue, 19 Dec 2023 10:13:42 +0100 Subject: [PATCH 002/104] tests passing; fix add_conditions --- ConfigSpace/c_util.py | 25 +- ConfigSpace/conditions.py | 157 +++++------ ConfigSpace/configuration.py | 13 +- ConfigSpace/configuration_space.py | 45 ++- ConfigSpace/forbidden.py | 34 +-- ConfigSpace/functional.py | 2 +- ConfigSpace/hyperparameters/__init__.py | 27 +- ConfigSpace/hyperparameters/beta_float.py | 25 +- ConfigSpace/hyperparameters/beta_integer.py | 18 +- ConfigSpace/hyperparameters/categorical.py | 175 ++++++------ ConfigSpace/hyperparameters/constant.py | 32 +-- .../hyperparameters/float_hyperparameter.py | 16 +- ConfigSpace/hyperparameters/hyperparameter.py | 43 ++- .../hyperparameters/integer_hyperparameter.py | 18 +- ConfigSpace/hyperparameters/normal_float.py | 75 ++--- ConfigSpace/hyperparameters/normal_integer.py | 49 ++-- ConfigSpace/hyperparameters/numerical.py | 26 +- ConfigSpace/hyperparameters/ordinal.py | 122 ++++---- ConfigSpace/hyperparameters/uniform_float.py | 65 +++-- .../hyperparameters/uniform_integer.py | 49 ++-- ConfigSpace/read_and_write/json.py | 9 +- ConfigSpace/read_and_write/pcs.py | 4 +- ConfigSpace/read_and_write/pcs_new.py | 4 +- ConfigSpace/util.py | 13 +- test/test_conditions.py | 4 - test/test_configuration_space.py | 34 ++- test/test_hyperparameters.py | 262 +++++------------- 27 files changed, 613 insertions(+), 733 deletions(-) diff --git a/ConfigSpace/c_util.py b/ConfigSpace/c_util.py index 5603c866..61ea61f1 100644 --- a/ConfigSpace/c_util.py +++ b/ConfigSpace/c_util.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections import deque +from typing import TYPE_CHECKING import numpy as np @@ -11,9 +12,11 @@ IllegalValueError, InactiveHyperparameterSetError, ) -from ConfigSpace.forbidden import AbstractForbiddenComponent from ConfigSpace.hyperparameters import Hyperparameter -from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter + +if TYPE_CHECKING: + from ConfigSpace.forbidden import AbstractForbiddenComponent + from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter def check_forbidden(forbidden_clauses: list, vector: np.ndarray) -> int: @@ -30,7 +33,7 @@ def check_configuration( self, vector: np.ndarray, allow_inactive_with_values: bool, -) -> int: +) -> None: hp_name: str hyperparameter: Hyperparameter hyperparameter_idx: int @@ -43,7 +46,7 @@ def check_configuration( inactive: set visited: set - active: np.ndarray = np.zeros(len(vector), dtype=int) + active: list[bool] = [False] * len(vector) unconditional_hyperparameters = self.get_all_unconditional_hyperparameters() to_visit = deque() @@ -52,7 +55,7 @@ def check_configuration( inactive = set() for ch in unconditional_hyperparameters: - active[self._hyperparameter_idx[ch]] = 1 + active[self._hyperparameter_idx[ch]] = True while len(to_visit) > 0: hp_name = to_visit.pop() @@ -70,16 +73,16 @@ def check_configuration( conditions = self._parent_conditions_of[child.name] add = True for condition in conditions: - if not condition._evaluate_vector(vector): + if condition._evaluate_vector(vector) is False: add = False inactive.add(child.name) break if add: hyperparameter_idx = self._hyperparameter_idx[child.name] - active[hyperparameter_idx] = 1 + active[hyperparameter_idx] = True to_visit.appendleft(child.name) - if active[hp_idx] and np.isnan(hp_value): + if active[hp_idx] is True and np.isnan(hp_value): raise ActiveHyperparameterNotSetError(hyperparameter) for hp_idx in self._idx_to_hyperparameter: @@ -150,7 +153,7 @@ def correct_sampled_array( add = True for j in range(len(conditions)): condition = conditions[j] - if not condition._evaluate_vector(vector): + if condition._evaluate_vector(vector) is False: add = False vector[hyperparameter_idx] = NaN inactive.add(child_name) @@ -175,7 +178,7 @@ def correct_sampled_array( add = True for j in range(len(conditions)): condition = conditions[j] - if not condition._evaluate_vector(vector): + if condition._evaluate_vector(vector) is False: add = False vector[hyperparameter_idx] = NaN inactive.add(child_name) @@ -282,7 +285,7 @@ def change_hp_value( active = True for condition in conditions: - if not condition._evaluate_vector(configuration_array): + if condition._evaluate_vector(configuration_array) is False: active = False break diff --git a/ConfigSpace/conditions.py b/ConfigSpace/conditions.py index 475c74ef..caba3f2d 100644 --- a/ConfigSpace/conditions.py +++ b/ConfigSpace/conditions.py @@ -29,40 +29,44 @@ import copy import io +from abc import ABC, abstractmethod from itertools import combinations from typing import TYPE_CHECKING, Any import numpy as np +from ConfigSpace.hyperparameters.hyperparameter import Comparison + if TYPE_CHECKING: from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter -class ConditionComponent: - def __init__(self) -> None: - pass - - def __repr__(self) -> str: - pass - +class ConditionComponent(ABC): + @abstractmethod def set_vector_idx(self, hyperparameter_to_idx) -> None: pass + @abstractmethod def get_children_vector(self) -> list[int]: pass + @abstractmethod def get_parents_vector(self) -> list[int]: pass + @abstractmethod def get_children(self) -> list[ConditionComponent]: pass + @abstractmethod def get_parents(self) -> list[ConditionComponent]: pass + @abstractmethod def get_descendant_literal_conditions(self) -> list[AbstractCondition]: pass + @abstractmethod def evaluate( self, instantiated_parent_hyperparameter: dict[str, None | int | float | str], @@ -72,7 +76,8 @@ def evaluate( def evaluate_vector(self, instantiated_vector): return bool(self._evaluate_vector(instantiated_vector)) - def _evaluate_vector(self, value: np.ndarray) -> int: + @abstractmethod + def _evaluate_vector(self, value: np.ndarray) -> bool: pass def __hash__(self) -> int: @@ -93,10 +98,7 @@ def __init__(self, child: Hyperparameter, parent: Hyperparameter) -> None: def __eq__(self, other: Any) -> bool: """ - This method implements a comparison between self and another - object. - - Additionally, it defines the __ne__() as stated in the + Defines the __ne__() as stated in the documentation from python: By default, object implements __eq__() by using is, returning NotImplemented in the case of a false comparison: True if x is y else NotImplemented. @@ -105,12 +107,11 @@ def __eq__(self, other: Any) -> bool: """ if not isinstance(other, self.__class__): - return False + return NotImplemented - if self.child != other.child: - return False - elif self.parent != other.parent: + if self.child != other.child or self.parent != other.parent: return False + return self.value == other.value def set_vector_idx(self, hyperparameter_to_idx: dict): @@ -136,18 +137,23 @@ def evaluate( self, instantiated_parent_hyperparameter: dict[str, int | float | str], ) -> bool: - hp_name = self.parent.name - return self._evaluate(instantiated_parent_hyperparameter[hp_name]) + value = instantiated_parent_hyperparameter[self.parent.name] + if isinstance(value, (float, int, np.number)) and np.isnan(value): + return False + return self._evaluate(value) - def _evaluate_vector(self, instantiated_vector: np.ndarray) -> int: + def _evaluate_vector(self, instantiated_vector: np.ndarray) -> bool: if self.parent_vector_id is None: raise ValueError("Parent vector id should not be None when calling evaluate vector") + return self._inner_evaluate_vector(instantiated_vector[self.parent_vector_id]) + @abstractmethod def _evaluate(self, instantiated_parent_hyperparameter: str | int | float) -> bool: pass - def _inner_evaluate_vector(self, value) -> int: + @abstractmethod + def _inner_evaluate_vector(self, value) -> bool: pass @@ -186,9 +192,9 @@ def __init__( super().__init__(child, parent) if not parent.is_legal(value): raise ValueError( - "Hyperparameter '{}' is " - "conditional on the illegal value '{}' of " - "its parent hyperparameter '{}'".format(child.name, value, parent.name), + f"Hyperparameter '{child.name}' is " + f"conditional on the illegal value '{value}' of " + f"its parent hyperparameter '{parent.name}'", ) self.value = value self.vector_value = self.parent._inverse_transform(self.value) @@ -206,16 +212,12 @@ def __copy__(self): def _evaluate(self, value: str | float | int) -> bool: # No need to check if the value to compare is a legal value; either it # is equal (and thus legal), or it would evaluate to False anyway + return self.parent.compare(value, self.value) is Comparison.EQUAL - cmp = self.parent.compare(value, self.value) - return cmp == 0 - - def _inner_evaluate_vector(self, value) -> int: + def _inner_evaluate_vector(self, value) -> bool: # No need to check if the value to compare is a legal value; either it # is equal (and thus legal), or it would evaluate to False anyway - - cmp = self.parent.compare_vector(value, self.vector_value) - return cmp == 0 + return self.parent.compare_vector(value, self.vector_value) is Comparison.EQUAL class NotEqualsCondition(AbstractCondition): @@ -254,9 +256,9 @@ def __init__( super().__init__(child, parent) if not parent.is_legal(value): raise ValueError( - "Hyperparameter '{}' is " - "conditional on the illegal value '{}' of " - "its parent hyperparameter '{}'".format(child.name, value, parent.name), + f"Hyperparameter '{child.name}' is " + f"conditional on the illegal value '{value}' of " + f"its parent hyperparameter '{parent.name}'", ) self.value = value self.vector_value = self.parent._inverse_transform(self.value) @@ -275,15 +277,12 @@ def _evaluate(self, value: str | float | int) -> bool: if not self.parent.is_legal(value): return False - cmp = self.parent.compare(value, self.value) - return cmp != 0 + return self.parent.compare(value, self.value) is not Comparison.EQUAL - def _inner_evaluate_vector(self, value) -> int: + def _inner_evaluate_vector(self, value) -> bool: if not self.parent.is_legal_vector(value): return False - - cmp = self.parent.compare_vector(value, self.vector_value) - return cmp != 0 + return self.parent.compare_vector(value, self.vector_value) is not Comparison.EQUAL class LessThanCondition(AbstractCondition): @@ -323,9 +322,9 @@ def __init__( self.parent.allow_greater_less_comparison() if not parent.is_legal(value): raise ValueError( - "Hyperparameter '{}' is " - "conditional on the illegal value '{}' of " - "its parent hyperparameter '{}'".format(child.name, value, parent.name), + f"Hyperparameter '{child.name}' is " + f"conditional on the illegal value '{value}' of " + f"its parent hyperparameter '{parent.name}'", ) self.value = value self.vector_value = self.parent._inverse_transform(self.value) @@ -344,15 +343,13 @@ def _evaluate(self, value: str | float | int) -> bool: if not self.parent.is_legal(value): return False - cmp = self.parent.compare(value, self.value) - return cmp == -1 + return self.parent.compare(value, self.value) is Comparison.LESS_THAN - def _inner_evaluate_vector(self, value) -> int: + def _inner_evaluate_vector(self, value) -> bool: if not self.parent.is_legal_vector(value): return False - cmp = self.parent.compare_vector(value, self.vector_value) - return cmp == -1 + return self.parent.compare_vector(value, self.vector_value) is Comparison.LESS_THAN class GreaterThanCondition(AbstractCondition): @@ -393,9 +390,9 @@ def __init__( self.parent.allow_greater_less_comparison() if not parent.is_legal(value): raise ValueError( - "Hyperparameter '{}' is " - "conditional on the illegal value '{}' of " - "its parent hyperparameter '{}'".format(child.name, value, parent.name), + f"Hyperparameter '{child.name}' is " + f"conditional on the illegal value '{value}' of " + f"its parent hyperparameter '{parent.name}'", ) self.value = value self.vector_value = self.parent._inverse_transform(self.value) @@ -414,15 +411,13 @@ def _evaluate(self, value: None | str | float | int) -> bool: if not self.parent.is_legal(value): return False - cmp = self.parent.compare(value, self.value) - return cmp == 1 + return self.parent.compare(value, self.value) is Comparison.GREATER_THAN def _inner_evaluate_vector(self, value) -> int: if not self.parent.is_legal_vector(value): return False - cmp = self.parent.compare_vector(value, self.vector_value) - return cmp == 1 + return self.parent.compare_vector(value, self.vector_value) is Comparison.GREATER_THAN class InCondition(AbstractCondition): @@ -463,9 +458,9 @@ def __init__( for value in values: if not parent.is_legal(value): raise ValueError( - "Hyperparameter '{}' is " - "conditional on the illegal value '{}' of " - "its parent hyperparameter '{}'".format(child.name, value, parent.name), + f"Hyperparameter '{child.name}' is " + f"conditional on the illegal value '{value}' of " + f"its parent hyperparameter '{parent.name}'", ) self.values = values self.value = values @@ -481,7 +476,7 @@ def __repr__(self) -> str: def _evaluate(self, value: str | float | int) -> bool: return value in self.values - def _inner_evaluate_vector(self, value) -> int: + def _inner_evaluate_vector(self, value) -> bool: return value in self.vector_values @@ -575,8 +570,6 @@ def evaluate( self, instantiated_hyperparameters: dict[str, None | int | float | str], ) -> bool: - values = np.empty(self.n_components, dtype=np.int32) - # Then, check if all parents were passed conditions = self.dlcs for condition in conditions: @@ -590,13 +583,14 @@ def evaluate( # Finally, call evaluate for all direct descendents and combine the # outcomes + values = np.empty(self.n_components, dtype=np.int32) for i, component in enumerate(self.components): e = component.evaluate(instantiated_hyperparameters) values[i] = e - return self._evaluate(self.n_components, values) + return self._evaluate(values) - def _evaluate_vector(self, instantiated_vector: np.ndarray) -> int: + def _evaluate_vector(self, instantiated_vector: np.ndarray) -> bool: values = np.empty(self.n_components, dtype=np.int32) # Finally, call evaluate for all direct descendents and combine the @@ -606,9 +600,10 @@ def _evaluate_vector(self, instantiated_vector: np.ndarray) -> int: e = component._evaluate_vector(instantiated_vector) values[i] = e - return self._evaluate(self.n_components, values) + return self._evaluate(values) - def _evaluate(self, I: int, evaluations) -> int: + @abstractmethod + def _evaluate(self, evaluations) -> bool: pass @@ -657,20 +652,11 @@ def __repr__(self) -> str: retval.write(")") return retval.getvalue() - def _evaluate_vector(self, instantiated_vector: np.ndarray) -> int: - for i in range(self.n_components): - component = self.components[i] - e = component._evaluate_vector(instantiated_vector) - if e == 0: - return 0 - - return 1 + def _evaluate_vector(self, instantiated_vector: np.ndarray) -> bool: + return all(c._evaluate_vector(instantiated_vector) for c in self.components) - def _evaluate(self, I: int, evaluations) -> int: - for i in range(I): - if evaluations[i] == 0: - return 0 - return 1 + def _evaluate(self, evaluations: np.ndarray) -> bool: + return bool(evaluations.all()) class OrConjunction(AbstractConjunction): @@ -715,17 +701,8 @@ def __repr__(self) -> str: retval.write(")") return retval.getvalue() - def _evaluate(self, I: int, evaluations) -> int: - for i in range(I): - if evaluations[i] == 1: - return 1 - return 0 - - def _evaluate_vector(self, instantiated_vector: np.ndarray) -> int: - for i in range(self.n_components): - component = self.components[i] - e = component._evaluate_vector(instantiated_vector) - if e == 1: - return 1 + def _evaluate(self, evaluations) -> bool: + return any(evaluations) - return 0 + def _evaluate_vector(self, instantiated_vector: np.ndarray) -> bool: + return any(c._evaluate_vector(instantiated_vector) for c in self.components) diff --git a/ConfigSpace/configuration.py b/ConfigSpace/configuration.py index 2d82c38e..463c560c 100644 --- a/ConfigSpace/configuration.py +++ b/ConfigSpace/configuration.py @@ -8,11 +8,11 @@ from ConfigSpace import c_util from ConfigSpace.exceptions import HyperparameterNotFoundError, IllegalValueError from ConfigSpace.hyperparameters import FloatHyperparameter +from ConfigSpace.hyperparameters.hyperparameter import NotSet if TYPE_CHECKING: from ConfigSpace.configuration_space import ConfigurationSpace - class Configuration(Mapping[str, Any]): def __init__( self, @@ -78,18 +78,19 @@ def __init__( # 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) + self._vector = np.empty(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 + value = values.get(key, NotSet) + if value is NotSet: + self._vector[i] = np.nan continue if not hp.is_legal(value): raise IllegalValueError(hp, value) # Truncate the float to be of constant length for a python version + # TODO: Optimize this if isinstance(hp, FloatHyperparameter): value = float(repr(value)) @@ -229,7 +230,7 @@ def __hash__(self) -> int: def __repr__(self) -> str: values = dict(self) header = "Configuration(values={" - lines = [f" '{key}': {repr(values[key])}," for key in sorted(values.keys())] + lines = [f" '{key}': {values[key]!r}," for key in sorted(values.keys())] end = "})" return "\n".join([header, *lines, end]) diff --git a/ConfigSpace/configuration_space.py b/ConfigSpace/configuration_space.py index 3b65efd6..d121bb52 100644 --- a/ConfigSpace/configuration_space.py +++ b/ConfigSpace/configuration_space.py @@ -33,8 +33,7 @@ import warnings from collections import OrderedDict, defaultdict, deque from itertools import chain -from typing import Any, Iterable, Iterator, KeysView, Mapping, cast, overload -from typing_extensions import Final +from typing import Any, Final, Iterable, Iterator, KeysView, Mapping, cast, overload import numpy as np @@ -73,6 +72,7 @@ UniformFloatHyperparameter, UniformIntegerHyperparameter, ) +from ConfigSpace.hyperparameters.hyperparameter import NotSet _ROOT: Final = "__HPOlib_configuration_space_root__" @@ -346,20 +346,27 @@ def add_conditions( values = [] conditions_to_add = [] for condition in conditions: + # TODO: Need to check that we can't add a condition twice! + if isinstance(condition, AbstractCondition): edges.append((condition.parent, condition.child)) values.append(condition.value) conditions_to_add.append(condition) + elif isinstance(condition, AbstractConjunction): dlcs = condition.get_descendant_literal_conditions() edges.extend([(dlc.parent, dlc.child) for dlc in dlcs]) values.extend([dlc.value for dlc in dlcs]) conditions_to_add.extend([condition] * len(dlcs)) + else: + raise TypeError(f"Unknown condition type {type(condition)}") + for edge, condition in zip(edges, conditions_to_add): self._check_condition(edge[1], condition) self._check_edges(edges, values) + print(conditions_to_add) for edge, condition in zip(edges, conditions_to_add): self._add_edge(edge[0], edge[1], condition) @@ -1140,7 +1147,7 @@ def __getitem__(self, key: str) -> Hyperparameter: return hp - #def __contains__(self, key: str) -> bool: + # def __contains__(self, key: str) -> bool: # return key in self._hyperparameters def __repr__(self) -> str: @@ -1314,12 +1321,18 @@ def _add_edge( child_node: Hyperparameter, condition: ConditionComponent, ) -> None: - with contextlib.suppress(Exception): - # TODO maybe this has to be done more carefully - del self._children[_ROOT][child_node.name] + self._children[_ROOT].pop(child_node.name, None) + self._parents[child_node.name].pop(_ROOT, None) + + if ( + existing := self._children[parent_node.name].pop(child_node.name, None) + ) is not None and existing != condition: + raise AmbiguousConditionError(existing, condition) - with contextlib.suppress(Exception): - del self._parents[child_node.name][_ROOT] + if ( + existing := self._parents[child_node.name].pop(parent_node.name, None) + ) is not None and existing != condition: + raise AmbiguousConditionError(existing, condition) self._children[parent_node.name][child_node.name] = condition self._parents[child_node.name][parent_node.name] = condition @@ -1453,23 +1466,25 @@ def _check_default_configuration(self) -> Configuration: instantiated_hyperparameters: dict[str, int | float | str | None] = {} for hp in self.values(): conditions = self._get_parent_conditions_of(hp.name) - active = True + active: bool = True + for condition in conditions: - parent_names = [ + parent_names = ( c.parent.name for c in condition.get_descendant_literal_conditions() - ] - + ) parents = { parent_name: instantiated_hyperparameters[parent_name] for parent_name in parent_names } - if not condition.evaluate(parents): - # TODO find out why a configuration is illegal! + # OPTIM: Can speed up things here by just breaking early? + if condition.evaluate(parents) is False: active = False if not active: - instantiated_hyperparameters[hp.name] = None + # the evaluate above will use compares so we need to use None + # and replace later.... + instantiated_hyperparameters[hp.name] = NotSet elif isinstance(hp, Constant): instantiated_hyperparameters[hp.name] = hp.value else: diff --git a/ConfigSpace/forbidden.py b/ConfigSpace/forbidden.py index 19677228..fb1cb91d 100644 --- a/ConfigSpace/forbidden.py +++ b/ConfigSpace/forbidden.py @@ -33,7 +33,6 @@ import numpy as np -from ConfigSpace.forbidden import AbstractForbiddenComponent from ConfigSpace.hyperparameters import Hyperparameter @@ -58,12 +57,7 @@ def __eq__(self, other: Any) -> bool: """ if not isinstance(other, self.__class__): - return False - - if self.value is None: - self.value = self.values - if other.value is None: - other.value = other.values + return NotImplemented return self.value == other.value and self.hyperparameter.name == other.hyperparameter.name @@ -110,8 +104,8 @@ def __init__(self, hyperparameter: Hyperparameter, value: Any) -> None: if not self.hyperparameter.is_legal(value): raise ValueError( "Forbidden clause must be instantiated with a " - "legal hyperparameter value for '{}', but got " - "'{}'".format(self.hyperparameter, str(value)), + f"legal hyperparameter value for '{self.hyperparameter}', but got " + f"'{value!s}'", ) self.value = value self.vector_value = self.hyperparameter._inverse_transform(self.value) @@ -167,8 +161,8 @@ def __init__(self, hyperparameter: Hyperparameter, values: Any) -> None: if not self.hyperparameter.is_legal(value): raise ValueError( "Forbidden clause must be instantiated with a " - "legal hyperparameter value for '{}', but got " - "'{}'".format(self.hyperparameter, str(value)), + f"legal hyperparameter value for '{self.hyperparameter}', but got " + f"'{value!s}'", ) self.values = values self.vector_values = [ @@ -181,6 +175,15 @@ def __copy__(self): values=copy.deepcopy(self.values), ) + def __eq__(self, other: Any) -> bool: + if not isinstance(other, self.__class__): + return NotImplemented + + return ( + self.hyperparameter == other.hyperparameter + and self.values == other.values + ) + def is_forbidden(self, instantiated_hyperparameters, strict) -> bool: value = instantiated_hyperparameters.get(self.hyperparameter.name) if value is None: @@ -191,8 +194,7 @@ def is_forbidden(self, instantiated_hyperparameters, strict) -> bool: "forbidden clause; you are missing " "'%s'." % self.hyperparameter.name, ) - else: - return False + return False return self._is_forbidden(value) @@ -323,9 +325,7 @@ def __copy__(self): return self.__class__([copy(comp) for comp in self.components]) def __eq__(self, other: Any) -> bool: - """ - This method implements a comparison between self and another - object. + """Comparison between self and another object. Additionally, it defines the __ne__() as stated in the documentation from python: @@ -335,7 +335,7 @@ def __eq__(self, other: Any) -> bool: unless it is NotImplemented. """ if not isinstance(other, self.__class__): - return False + return NotImplemented if self.n_components != other.n_components: return False diff --git a/ConfigSpace/functional.py b/ConfigSpace/functional.py index 4abeed3b..e8da4045 100644 --- a/ConfigSpace/functional.py +++ b/ConfigSpace/functional.py @@ -77,7 +77,7 @@ def arange_chunked( n_items = int(np.ceil((stop - start) / step)) n_chunks = int(np.ceil(n_items / chunk_size)) - for chunk in range(0, n_chunks): + for chunk in range(n_chunks): chunk_start = start + (chunk * chunk_size) chunk_stop = min(chunk_start + chunk_size, stop) yield np.arange(chunk_start, chunk_stop, step) diff --git a/ConfigSpace/hyperparameters/__init__.py b/ConfigSpace/hyperparameters/__init__.py index e8410058..ef85f8b4 100644 --- a/ConfigSpace/hyperparameters/__init__.py +++ b/ConfigSpace/hyperparameters/__init__.py @@ -1,16 +1,16 @@ -from .beta_float import BetaFloatHyperparameter -from .beta_integer import BetaIntegerHyperparameter -from .categorical import CategoricalHyperparameter -from .constant import Constant, UnParametrizedHyperparameter -from .float_hyperparameter import FloatHyperparameter -from .hyperparameter import Hyperparameter -from .integer_hyperparameter import IntegerHyperparameter -from .normal_float import NormalFloatHyperparameter -from .normal_integer import NormalIntegerHyperparameter -from .numerical import NumericalHyperparameter -from .ordinal import OrdinalHyperparameter -from .uniform_float import UniformFloatHyperparameter -from .uniform_integer import UniformIntegerHyperparameter +from ConfigSpace.hyperparameters.beta_float import BetaFloatHyperparameter +from ConfigSpace.hyperparameters.beta_integer import BetaIntegerHyperparameter +from ConfigSpace.hyperparameters.categorical import CategoricalHyperparameter +from ConfigSpace.hyperparameters.constant import Constant, UnParametrizedHyperparameter +from ConfigSpace.hyperparameters.float_hyperparameter import FloatHyperparameter +from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter, NotSet +from ConfigSpace.hyperparameters.integer_hyperparameter import IntegerHyperparameter +from ConfigSpace.hyperparameters.normal_float import NormalFloatHyperparameter +from ConfigSpace.hyperparameters.normal_integer import NormalIntegerHyperparameter +from ConfigSpace.hyperparameters.numerical import NumericalHyperparameter +from ConfigSpace.hyperparameters.ordinal import OrdinalHyperparameter +from ConfigSpace.hyperparameters.uniform_float import UniformFloatHyperparameter +from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter __all__ = [ "Hyperparameter", @@ -27,4 +27,5 @@ "NormalIntegerHyperparameter", "BetaFloatHyperparameter", "BetaIntegerHyperparameter", + "NotSet", ] diff --git a/ConfigSpace/hyperparameters/beta_float.py b/ConfigSpace/hyperparameters/beta_float.py index 36203e22..1680eef7 100644 --- a/ConfigSpace/hyperparameters/beta_float.py +++ b/ConfigSpace/hyperparameters/beta_float.py @@ -1,15 +1,16 @@ from __future__ import annotations import io -import warnings -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np from scipy.stats import beta as spbeta -from ConfigSpace.hyperparameters.beta_integer import BetaIntegerHyperparameter from ConfigSpace.hyperparameters.uniform_float import UniformFloatHyperparameter +if TYPE_CHECKING: + from ConfigSpace.hyperparameters.beta_integer import BetaIntegerHyperparameter + class BetaFloatHyperparameter(UniformFloatHyperparameter): def __init__( @@ -68,18 +69,24 @@ def __init__( # then actually call check_default once we have alpha and beta, and are not inside # UniformFloatHP. super().__init__( - name, lower, upper, (upper + lower) / 2, q, log, meta, + name=name, + lower=lower, + upper=upper, + default_value=(upper + lower) / 2, + q=q, + log=log, + meta=meta, ) self.alpha = float(alpha) self.beta = float(beta) if (alpha < 1) or (beta < 1): raise ValueError( - "Please provide values of alpha and beta larger than or equal to\ - 1 so that the probability density is finite.", + "Please provide values of alpha and beta larger than or equal to" + " 1 so that the probability density is finite.", ) - if (self.q is not None) and (self.log is not None) and (default_value is None): - warnings.warn( + if (self.q is not None) and self.log and (default_value is None): + raise ValueError( "Logscale and quantization together results in incorrect default values. " "We recommend specifying a default value manually for this specific case.", ) @@ -189,6 +196,8 @@ def to_integer(self) -> BetaIntegerHyperparameter: lower = int(np.ceil(self.lower)) upper = int(np.floor(self.upper)) default_value = int(np.rint(self.default_value)) + from ConfigSpace.hyperparameters.beta_integer import BetaIntegerHyperparameter + return BetaIntegerHyperparameter( self.name, lower=lower, diff --git a/ConfigSpace/hyperparameters/beta_integer.py b/ConfigSpace/hyperparameters/beta_integer.py index bb9172a6..25ed28d4 100644 --- a/ConfigSpace/hyperparameters/beta_integer.py +++ b/ConfigSpace/hyperparameters/beta_integer.py @@ -7,8 +7,8 @@ from scipy.stats import beta as spbeta from ConfigSpace.functional import arange_chunked -from ConfigSpace.hyperparameters import UniformIntegerHyperparameter from ConfigSpace.hyperparameters.beta_float import BetaFloatHyperparameter +from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter # OPTIM: Some operations generate an arange which could blowup memory if # done over the entire space of integers (int32/64). @@ -73,14 +73,20 @@ def __init__( """ super().__init__( - name, lower, upper, np.round((upper + lower) / 2), q, log, meta, + name, + lower, + upper, + np.round((upper + lower) / 2), + q, + log, + meta, ) self.alpha = float(alpha) self.beta = float(beta) if (alpha < 1) or (beta < 1): raise ValueError( - "Please provide values of alpha and beta larger than or equal to\ - 1 so that the probability density is finite.", + "Please provide values of alpha and beta larger than or equal to" + "1 so that the probability density is finite.", ) q = 1 if self.q is None else self.q self.bfhp = BetaFloatHyperparameter( @@ -185,7 +191,9 @@ def check_default(self, default_value: int | float | None) -> int: raise ValueError(f"Illegal default value {default_value}") def _sample( - self, rs: np.random.RandomState, size: int | None = None, + self, + rs: np.random.RandomState, + size: int | None = None, ) -> np.ndarray | float: value = self.bfhp._sample(rs, size=size) # Map all floats which belong to the same integer value to the same diff --git a/ConfigSpace/hyperparameters/categorical.py b/ConfigSpace/hyperparameters/categorical.py index 8cec9cc1..e6a25eae 100644 --- a/ConfigSpace/hyperparameters/categorical.py +++ b/ConfigSpace/hyperparameters/categorical.py @@ -1,24 +1,25 @@ -from collections import Counter +from __future__ import annotations + import copy import io -from typing import Any, Dict, List, Optional, Sequence, Tuple, Union +from collections import Counter +from typing import Any, Sequence import numpy as np -from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter +from ConfigSpace.hyperparameters.hyperparameter import Comparison, Hyperparameter class CategoricalHyperparameter(Hyperparameter): - # TODO add more magic for automated type recognition # TODO move from list to tuple for choices argument def __init__( self, name: str, - choices: Union[List[Union[str, float, int]], Tuple[Union[float, int, str]]], - default_value: Union[int, float, str, None] = None, - meta: Optional[Dict] = None, - weights: Optional[Sequence[Union[int, float]]] = None, + choices: list[str | float | int] | tuple[float | int | str], + default_value: int | float | str | None = None, + meta: dict | None = None, + weights: Sequence[int | float] | None = None, ) -> None: """ A categorical hyperparameter. @@ -49,8 +50,7 @@ def __init__( List of weights for the choices to be used (after normalization) as probabilities during sampling, no negative values allowed """ - - super(CategoricalHyperparameter, self).__init__(name, meta) + super().__init__(name, meta) # TODO check that there is no bullshit in the choices! counter = Counter(choices) for choice in choices: @@ -63,11 +63,15 @@ def __init__( if choice is None: raise TypeError("Choice 'None' is not supported") if isinstance(choices, set): - raise TypeError("Using a set of choices is prohibited as it can result in " - "non-deterministic behavior. Please use a list or a tuple.") + raise TypeError( + "Using a set of choices is prohibited as it can result in " + "non-deterministic behavior. Please use a list or a tuple.", + ) if isinstance(weights, set): - raise TypeError("Using a set of weights is prohibited as it can result in " - "non-deterministic behavior. Please use a list or a tuple.") + raise TypeError( + "Using a set of weights is prohibited as it can result in " + "non-deterministic behavior. Please use a list or a tuple.", + ) self.choices = tuple(choices) if weights is not None: self.weights = tuple(weights) @@ -122,8 +126,8 @@ def __eq__(self, other: Any) -> bool: ordered_probabilities_other = { choice: ( other.probabilities[other.choices.index(choice)] - if choice in other.choices else - None + if choice in other.choices + else None ) for choice in self.choices } @@ -131,21 +135,21 @@ def __eq__(self, other: Any) -> bool: ordered_probabilities_other = None return ( - self.name == other.name and - set(self.choices) == set(other.choices) and - self.default_value == other.default_value and - ( - (ordered_probabilities_self is None and ordered_probabilities_other is None) or - ordered_probabilities_self == ordered_probabilities_other or - ( + self.name == other.name + and set(self.choices) == set(other.choices) + and self.default_value == other.default_value + and ( + (ordered_probabilities_self is None and ordered_probabilities_other is None) + or ordered_probabilities_self == ordered_probabilities_other + or ( ordered_probabilities_self is None and len(np.unique(list(ordered_probabilities_other.values()))) == 1 - ) or - ( + ) + or ( ordered_probabilities_other is None and len(np.unique(list(ordered_probabilities_self.values()))) == 1 ) - ) + ) ) def __hash__(self): @@ -160,14 +164,14 @@ def __copy__(self): meta=self.meta, ) - def to_uniform(self) -> "CategoricalHyperparameter": + def to_uniform(self) -> CategoricalHyperparameter: """ Creates a categorical parameter with equal weights for all choices This is used for the uniform configspace when sampling configurations in the local search - in PiBO: https://openreview.net/forum?id=MMAeCXIa89 + in PiBO: https://openreview.net/forum?id=MMAeCXIa89. Returns - ---------- + ------- CategoricalHyperparameter An identical parameter as the original, except that all weights are uniform. """ @@ -178,35 +182,36 @@ def to_uniform(self) -> "CategoricalHyperparameter": meta=self.meta, ) - def compare(self, value: Union[int, float, str], value2: Union[int, float, str]) -> int: + def compare(self, value: int | float | str, value2: int | float | str) -> Comparison: if value == value2: - return 0 - else: - return 1 + return Comparison.EQUAL + + return Comparison.NOT_EQUAL - def compare_vector(self, DTYPE_t value, DTYPE_t value2) -> int: + def compare_vector(self, value: float, value2: float) -> Comparison: if value == value2: - return 0 - else: - return 1 + return Comparison.EQUAL - def is_legal(self, value: Union[None, str, float, int]) -> bool: - if value in self.choices: - return True - else: - return False + return Comparison.NOT_EQUAL - def is_legal_vector(self, DTYPE_t value) -> int: + def is_legal(self, value: None | str | float | int) -> bool: + return value in self.choices + + def is_legal_vector(self, value) -> int: return value in self._choices_set - def _get_probabilities(self, choices: Tuple[Union[None, str, float, int]], - weights: Union[None, List[float]]) -> Union[None, List[float]]: + def _get_probabilities( + self, + choices: tuple[None | str | float | int], + weights: None | list[float], + ) -> None | list[float]: if weights is None: return tuple(np.ones(len(choices)) / len(choices)) if len(weights) != len(choices): raise ValueError( - "The list of weights and the list of choices are required to be of same length.") + "The list of weights and the list of choices are required to be of same length.", + ) weights = np.array(weights) @@ -218,8 +223,10 @@ def _get_probabilities(self, choices: Tuple[Union[None, str, float, int]], return tuple(weights / np.sum(weights)) - def check_default(self, default_value: Union[None, str, float, int], - ) -> Union[str, float, int]: + def check_default( + self, + default_value: None | str | float | int, + ) -> str | float | int: if default_value is None: return self.choices[np.argmax(self.weights) if self.weights is not None else 0] elif self.is_legal(default_value): @@ -227,34 +234,43 @@ def check_default(self, default_value: Union[None, str, float, int], else: raise ValueError("Illegal default value %s" % str(default_value)) - def _sample(self, rs: np.random.RandomState, size: Optional[int] = None, - ) -> Union[int, np.ndarray]: + def _sample( + self, + rs: np.random.RandomState, + size: int | None = None, + ) -> int | np.ndarray: return rs.choice(a=self.num_choices, size=size, replace=True, p=self.probabilities) - def _transform_vector(self, vector: np.ndarray ) -> np.ndarray: + def _transform_vector(self, vector: np.ndarray) -> np.ndarray: if np.isnan(vector).any(): - raise ValueError("Vector %s contains NaN\'s" % vector) + raise ValueError("Vector %s contains NaN's" % vector) if np.equal(np.mod(vector, 1), 0): return self.choices[vector.astype(int)] - raise ValueError("Can only index the choices of the ordinal " - "hyperparameter %s with an integer, but provided " - "the following float: %f" % (self, vector)) + raise ValueError( + "Can only index the choices of the ordinal " + f"hyperparameter {self} with an integer, but provided " + f"the following float: {vector:f}", + ) - def _transform_scalar(self, scalar: Union[float, int]) -> Union[float, int, str]: + def _transform_scalar(self, scalar: float | int) -> float | int | str: if scalar != scalar: raise ValueError("Number %s is NaN" % scalar) if scalar % 1 == 0: return self.choices[int(scalar)] - raise ValueError("Can only index the choices of the ordinal " - "hyperparameter %s with an integer, but provided " - "the following float: %f" % (self, scalar)) + raise ValueError( + "Can only index the choices of the ordinal " + f"hyperparameter {self} with an integer, but provided " + f"the following float: {scalar:f}", + ) - def _transform(self, vector: Union[np.ndarray, float, int, str], - ) -> Optional[Union[np.ndarray, float, int]]: + def _transform( + self, + vector: np.ndarray | float | int | str, + ) -> np.ndarray | float | int | None: try: if isinstance(vector, np.ndarray): return self._transform_vector(vector) @@ -262,7 +278,7 @@ def _transform(self, vector: Union[np.ndarray, float, int, str], except ValueError: return None - def _inverse_transform(self, vector: Union[None, str, float, int]) -> Union[int, float]: + def _inverse_transform(self, vector: None | str | float | int) -> int | float: if vector is None: return np.NaN return self.choices.index(vector) @@ -270,12 +286,16 @@ def _inverse_transform(self, vector: Union[None, str, float, int]) -> Union[int, def has_neighbors(self) -> bool: return len(self.choices) > 1 - def get_num_neighbors(self, value = None) -> int: + def get_num_neighbors(self, value=None) -> int: return len(self.choices) - 1 - def get_neighbors(self, value: int, rs: np.random.RandomState, - number: Union[int, float] = np.inf, transform: bool = False, - ) -> List[Union[float, int, str]]: + def get_neighbors( + self, + value: int, + rs: np.random.RandomState, + number: int | float = np.inf, + transform: bool = False, + ) -> list[float | int | str]: neighbors = [] # type: List[Union[float, int, str]] if number < len(self.choices): while len(neighbors) < number: @@ -286,17 +306,14 @@ def get_neighbors(self, value: int, rs: np.random.RandomState, if neighbor_idx != index: rejected = False - if transform: - candidate = self._transform(neighbor_idx) - else: - candidate = float(neighbor_idx) + candidate = self._transform(neighbor_idx) if transform else float(neighbor_idx) if candidate in neighbors: continue else: neighbors.append(candidate) else: - for candidate_idx, candidate_value in enumerate(self.choices): + for candidate_idx, _candidate_value in enumerate(self.choices): if int(value) == candidate_idx: continue else: @@ -310,11 +327,13 @@ def get_neighbors(self, value: int, rs: np.random.RandomState, return neighbors def allow_greater_less_comparison(self) -> bool: - raise ValueError("Parent hyperparameter in a > or < " - "condition must be a subclass of " - "NumericalHyperparameter or " - "OrdinalHyperparameter, but is " - "") + raise ValueError( + "Parent hyperparameter in a > or < " + "condition must be a subclass of " + "NumericalHyperparameter or " + "OrdinalHyperparameter, but is " + "", + ) def pdf(self, vector: np.ndarray) -> np.ndarray: """ @@ -332,7 +351,7 @@ def pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ @@ -360,7 +379,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ diff --git a/ConfigSpace/hyperparameters/constant.py b/ConfigSpace/hyperparameters/constant.py index f55a85ba..5a9c04a7 100644 --- a/ConfigSpace/hyperparameters/constant.py +++ b/ConfigSpace/hyperparameters/constant.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Optional, Union +from typing import Any import numpy as np @@ -11,8 +11,8 @@ class Constant(Hyperparameter): def __init__( self, name: str, - value: Union[str, int, float], - meta: Optional[dict] = None, + value: str | int | float, + meta: dict | None = None, ) -> None: """ Representing a constant hyperparameter in the configuration space. @@ -30,13 +30,13 @@ def __init__( Field for holding meta data provided by the user. Not used by the configuration space. """ - super(Constant, self).__init__(name, meta) + super().__init__(name, meta) allowed_types = (int, float, str) if not isinstance(value, allowed_types) or isinstance(value, bool): raise TypeError( - "Constant value is of type %s, but only the " - "following types are allowed: %s" % (type(value), allowed_types), + f"Constant value is of type {type(value)}, but only the " + f"following types are allowed: {allowed_types}", ) # type: ignore self.value = value @@ -76,34 +76,34 @@ def __copy__(self): def __hash__(self): return hash((self.name, self.value)) - def is_legal(self, value: Union[str, int, float]) -> bool: + def is_legal(self, value: str | int | float) -> bool: return value == self.value def is_legal_vector(self, value) -> int: return value == self.value_vector - def _sample(self, rs: None, size: Optional[int] = None) -> Union[int, np.ndarray]: + def _sample(self, rs: None, size: int | None = None) -> int | np.ndarray: return 0 if size == 1 else np.zeros((size,)) def _transform( - self, vector: Optional[Union[np.ndarray, float, int]], - ) -> Optional[Union[np.ndarray, float, int]]: + self, vector: np.ndarray | float | int | None, + ) -> np.ndarray | float | int | None: return self.value def _transform_vector( - self, vector: Optional[np.ndarray], - ) -> Optional[Union[np.ndarray, float, int]]: + self, vector: np.ndarray | None, + ) -> np.ndarray | float | int | None: return self.value def _transform_scalar( - self, vector: Optional[Union[float, int]], - ) -> Optional[Union[np.ndarray, float, int]]: + self, vector: float | int | None, + ) -> np.ndarray | float | int | None: return self.value def _inverse_transform( self, - vector: Union[np.ndarray, float, int], - ) -> Union[np.ndarray, int, float]: + vector: np.ndarray | float | int, + ) -> np.ndarray | int | float: if vector != self.value: return np.NaN return 0 diff --git a/ConfigSpace/hyperparameters/float_hyperparameter.py b/ConfigSpace/hyperparameters/float_hyperparameter.py index 7e709056..1f3ba662 100644 --- a/ConfigSpace/hyperparameters/float_hyperparameter.py +++ b/ConfigSpace/hyperparameters/float_hyperparameter.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Optional, Union - import numpy as np from ConfigSpace.hyperparameters.numerical import NumericalHyperparameter @@ -11,24 +9,24 @@ class FloatHyperparameter(NumericalHyperparameter): def __init__( self, name: str, - default_value: Union[int, float], - meta: Optional[dict] = None, + default_value: int | float, + meta: dict | None = None, ) -> None: - super(FloatHyperparameter, self).__init__(name, default_value, meta) + super().__init__(name, default_value, meta) - def is_legal(self, value: Union[int, float]) -> bool: + def is_legal(self, value: int | float) -> bool: raise NotImplementedError() def is_legal_vector(self, value) -> int: raise NotImplementedError() - def check_default(self, default_value: Union[int, float]) -> float: + def check_default(self, default_value: int | float) -> float: raise NotImplementedError() def _transform( self, - vector: Union[np.ndarray, float, int], - ) -> Optional[Union[np.ndarray, float, int]]: + vector: np.ndarray | float | int, + ) -> np.ndarray | float | int | None: try: if isinstance(vector, np.ndarray): return self._transform_vector(vector) diff --git a/ConfigSpace/hyperparameters/hyperparameter.py b/ConfigSpace/hyperparameters/hyperparameter.py index 0e935f50..7a998095 100644 --- a/ConfigSpace/hyperparameters/hyperparameter.py +++ b/ConfigSpace/hyperparameters/hyperparameter.py @@ -1,15 +1,27 @@ -from typing import Dict, Optional, Union +from __future__ import annotations + +from enum import Enum, auto import numpy as np +NotSet = object() -class Hyperparameter: +class Comparison(Enum): + """Enumeration of possible comparison results.""" + + LESS_THAN = auto() + EQUAL = auto() + GREATER_THAN = auto() + NOT_EQUAL = auto() - def __init__(self, name: str, meta: Optional[Dict]) -> None: + +class Hyperparameter: + def __init__(self, name: str, meta: dict | None) -> None: if not isinstance(name, str): raise TypeError( "The name of a hyperparameter must be an instance of" - " %s, but is %s." % (str(str), type(name))) + f" {str!s}, but is {type(name)}.", + ) self.name: str = name self.meta = meta @@ -43,9 +55,9 @@ def sample(self, rs): def rvs( self, - size: Optional[int] = None, - random_state: Optional[Union[int, np.random, np.random.RandomState]] = None, - ) -> Union[float, np.ndarray]: + size: int | None = None, + random_state: int | np.random.RandomState | None = None, + ) -> float | np.ndarray: """ scipy compatibility wrapper for ``_sample``, allowing the hyperparameter to be used in sklearn API @@ -77,8 +89,9 @@ def check_random_state(seed): return seed except AttributeError: pass - raise ValueError("%r cannot be used to seed a numpy.random.RandomState" - " instance" % seed) + raise ValueError( + "%r cannot be used to seed a numpy.random.RandomState" " instance" % seed, + ) # if size=None, return a value, but if size=1, return a 1-element array @@ -96,8 +109,8 @@ def _sample(self, rs, size): def _transform( self, - vector: Union[np.ndarray, float, int], - ) -> Optional[Union[np.ndarray, float, int]]: + vector: np.ndarray | float | int, + ) -> np.ndarray | float | int | None: raise NotImplementedError() def _inverse_transform(self, vector): @@ -106,13 +119,13 @@ def _inverse_transform(self, vector): def has_neighbors(self): raise NotImplementedError() - def get_neighbors(self, value, rs, number, transform = False): + def get_neighbors(self, value, rs, number, transform=False): raise NotImplementedError() def get_num_neighbors(self, value): raise NotImplementedError() - def compare_vector(self, DTYPE_t value, DTYPE_t value2) -> int: + def compare_vector(self, value: float, value2: float) -> Comparison: raise NotImplementedError() def pdf(self, vector: np.ndarray) -> np.ndarray: @@ -131,7 +144,7 @@ def pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ @@ -152,7 +165,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: function is to be computed. Returns - ---------- + ------- np.ndarray(N, ) Probability density values of the input vector """ diff --git a/ConfigSpace/hyperparameters/integer_hyperparameter.py b/ConfigSpace/hyperparameters/integer_hyperparameter.py index 87a5f6e3..c594c4c1 100644 --- a/ConfigSpace/hyperparameters/integer_hyperparameter.py +++ b/ConfigSpace/hyperparameters/integer_hyperparameter.py @@ -1,15 +1,13 @@ from __future__ import annotations -from typing import Optional, Union - import numpy as np from ConfigSpace.hyperparameters.numerical import NumericalHyperparameter class IntegerHyperparameter(NumericalHyperparameter): - def __init__(self, name: str, default_value: int, meta: Optional[dict] = None) -> None: - super(IntegerHyperparameter, self).__init__(name, default_value, meta) + def __init__(self, name: str, default_value: int, meta: dict | None = None) -> None: + super().__init__(name, default_value, meta) def is_legal(self, value: int) -> bool: raise NotImplementedError @@ -23,16 +21,16 @@ def check_default(self, default_value) -> int: def check_int(self, parameter: int, name: str) -> int: if abs(int(parameter) - parameter) > 0.00000001 and type(parameter) is not int: raise ValueError( - "For the Integer parameter %s, the value must be " - "an Integer, too. Right now it is a %s with value" - " %s." % (name, type(parameter), str(parameter)), + f"For the Integer parameter {name}, the value must be " + f"an Integer, too. Right now it is a {type(parameter)} with value" + f" {parameter!s}.", ) return int(parameter) def _transform( self, - vector: Union[np.ndarray, float, int], - ) -> Optional[Union[np.ndarray, float, int]]: + vector: np.ndarray | float | int, + ) -> np.ndarray | float | int | None: try: if isinstance(vector, np.ndarray): return self._transform_vector(vector) @@ -80,7 +78,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: distributions, only normal distributions (as the inverse_transform in the pdf method handles these). Optimally, an IntegerHyperparameter should have a corresponding float, which can be utlized for the calls - to the probability density function (see e.g. NormalIntegerHyperparameter) + to the probability density function (see e.g. NormalIntegerHyperparameter). Parameters ---------- diff --git a/ConfigSpace/hyperparameters/normal_float.py b/ConfigSpace/hyperparameters/normal_float.py index f8dcfc40..31055f28 100644 --- a/ConfigSpace/hyperparameters/normal_float.py +++ b/ConfigSpace/hyperparameters/normal_float.py @@ -2,28 +2,30 @@ import io import math -from typing import Any, Optional, Union +from typing import TYPE_CHECKING, Any import numpy as np from scipy.stats import norm, truncnorm from ConfigSpace.hyperparameters.float_hyperparameter import FloatHyperparameter -from ConfigSpace.hyperparameters.normal_integer import NormalIntegerHyperparameter from ConfigSpace.hyperparameters.uniform_float import UniformFloatHyperparameter +if TYPE_CHECKING: + from ConfigSpace.hyperparameters.normal_integer import NormalIntegerHyperparameter + class NormalFloatHyperparameter(FloatHyperparameter): def __init__( self, name: str, - mu: Union[int, float], - sigma: Union[int, float], - default_value: Union[None, float] = None, - q: Union[int, float, None] = None, + mu: int | float, + sigma: int | float, + default_value: None | float = None, + q: int | float | None = None, log: bool = False, - lower: Optional[Union[float, int]] = None, - upper: Optional[Union[float, int]] = None, - meta: Optional[dict] = None, + lower: float | int | None = None, + upper: float | int | None = None, + meta: dict | None = None, ) -> None: r""" A normally distributed float hyperparameter. @@ -59,13 +61,12 @@ def __init__( Field for holding meta data provided by the user. Not used by the configuration space. """ - super(NormalFloatHyperparameter, self).__init__(name, default_value, meta) self.mu = float(mu) self.sigma = float(sigma) self.q = float(q) if q is not None else None self.log = bool(log) - self.default_value = self.check_default(default_value) - self.normalized_default_value = self._inverse_transform(self.default_value) + self.lower: float | None = None + self.upper: float | None = None if (lower is not None) ^ (upper is not None): raise ValueError( @@ -78,13 +79,13 @@ def __init__( if self.lower >= self.upper: raise ValueError( - "Upper bound %f must be larger than lower bound " - "%f for hyperparameter %s" % (self.upper, self.lower, name), + f"Upper bound {self.upper:f} must be larger than lower bound " + f"{self.lower:f} for hyperparameter {name}", ) - elif log and self.lower <= 0: + if log and self.lower <= 0: raise ValueError( - "Negative lower bound (%f) for log-scale " - "hyperparameter %s is forbidden." % (self.lower, name), + f"Negative lower bound ({self.lower:f}) for log-scale " + f"hyperparameter {name} is forbidden.", ) self.default_value = self.check_default(default_value) @@ -105,28 +106,30 @@ def __init__( else: self._lower = self.lower self._upper = self.upper + if self.q is not None: # There can be weird rounding errors, so we compare the result against self.q, see # In [13]: 2.4 % 0.2 # Out[13]: 0.1999999999999998 if np.round((self.upper - self.lower) % self.q, 10) not in (0, self.q): raise ValueError( - "Upper bound (%f) - lower bound (%f) must be a multiple of q (%f)" - % (self.upper, self.lower, self.q), + f"Upper bound ({self.upper:f}) - lower bound ({self.lower:f}) must be a multiple of q ({self.q:f})", ) + default_value = self.check_default(default_value) + super().__init__(name, default_value, meta) + self.normalized_default_value = self._inverse_transform(self.default_value) + def __repr__(self) -> str: repr_str = io.StringIO() if self.lower is None or self.upper is None: repr_str.write( - "%s, Type: NormalFloat, Mu: %s Sigma: %s, Default: %s" - % (self.name, repr(self.mu), repr(self.sigma), repr(self.default_value)), + f"{self.name}, Type: NormalFloat, Mu: {self.mu!r} Sigma: {self.sigma!r}, Default: {self.default_value!r}", ) else: repr_str.write( - "%s, Type: NormalFloat, Mu: %s Sigma: %s, Range: [%s, %s], Default: %s" - % ( + "{}, Type: NormalFloat, Mu: {} Sigma: {}, Range: [{}, {}], Default: {}".format( self.name, repr(self.mu), repr(self.sigma), @@ -204,7 +207,7 @@ def to_uniform(self, z: int = 3) -> UniformFloatHyperparameter: meta=self.meta, ) - def check_default(self, default_value: Union[int, float]) -> Union[int, float]: + def check_default(self, default_value: int | float | None) -> int | float: if default_value is None: if self.log: return self._transform_scalar(self.mu) @@ -217,10 +220,7 @@ def check_default(self, default_value: Union[int, float]) -> Union[int, float]: raise ValueError("Illegal default value %s" % str(default_value)) def to_integer(self) -> NormalIntegerHyperparameter: - if self.q is None: - q_int = None - else: - q_int = int(np.rint(self.q)) + q_int = None if self.q is None else int(np.rint(self.q)) if self.lower is None: lower = None upper = None @@ -228,6 +228,8 @@ def to_integer(self) -> NormalIntegerHyperparameter: lower = np.ceil(self.lower) upper = np.floor(self.upper) + from ConfigSpace.hyperparameters.normal_integer import NormalIntegerHyperparameter + return NormalIntegerHyperparameter( self.name, int(np.rint(self.mu)), @@ -252,8 +254,8 @@ def is_legal_vector(self, value) -> int: def _sample( self, rs: np.random.RandomState, - size: Optional[int] = None, - ) -> Union[np.ndarray, float]: + size: int | None = None, + ) -> np.ndarray | float: if self.lower is None: mu = self.mu sigma = self.sigma @@ -287,8 +289,9 @@ def _transform_scalar(self, scalar: float) -> float: return scalar def _inverse_transform( - self, vector: Union[float, np.ndarray, None], - ) -> Union[float, np.ndarray]: + self, + vector: float | np.ndarray | None, + ) -> float | np.ndarray: # TODO: Should probably use generics here if vector is None: return np.NaN @@ -299,10 +302,14 @@ def _inverse_transform( return vector def get_neighbors( - self, value: float, rs: np.random.RandomState, number: int = 4, transform: bool = False, + self, + value: float, + rs: np.random.RandomState, + number: int = 4, + transform: bool = False, ) -> list[float]: neighbors = [] - for i in range(number): + for _i in range(number): new_value = rs.normal(value, self.sigma) if self.lower is not None and self.upper is not None: diff --git a/ConfigSpace/hyperparameters/normal_integer.py b/ConfigSpace/hyperparameters/normal_integer.py index ea8488ad..4e8c3805 100644 --- a/ConfigSpace/hyperparameters/normal_integer.py +++ b/ConfigSpace/hyperparameters/normal_integer.py @@ -3,7 +3,7 @@ import io import warnings from itertools import count -from typing import Any, Optional, Union +from typing import Any import numpy as np from more_itertools import roundrobin @@ -29,13 +29,13 @@ def __init__( self, name: str, mu: int, - sigma: Union[int, float], - default_value: Union[int, None] = None, - q: Union[None, int] = None, + sigma: int | float, + default_value: int | None = None, + q: None | int = None, log: bool = False, - lower: Optional[int] = None, - upper: Optional[int] = None, - meta: Optional[dict] = None, + lower: int | None = None, + upper: int | None = None, + meta: dict | None = None, ) -> None: r""" A normally distributed integer hyperparameter. @@ -73,7 +73,7 @@ def __init__( Not used by the configuration space. """ - super(NormalIntegerHyperparameter, self).__init__(name, default_value, meta) + super().__init__(name, default_value, meta) self.mu = mu self.sigma = sigma @@ -99,6 +99,8 @@ def __init__( "Only one bound was provided when both lower and upper bounds must be provided.", ) + self.lower: int | None = None + self.upper: int | None = None if lower is not None and upper is not None: self.upper = self.check_int(upper, "upper") self.lower = self.check_int(lower, "lower") @@ -107,13 +109,11 @@ def __init__( "Upper bound %d must be larger than lower bound " "%d for hyperparameter %s" % (self.lower, self.upper, name), ) - elif log and self.lower <= 0: + if log and self.lower <= 0: raise ValueError( "Negative lower bound (%d) for log-scale " "hyperparameter %s is forbidden." % (self.lower, name), ) - self.lower = lower - self.upper = upper self.nfhp = NormalFloatHyperparameter( self.name, @@ -140,13 +140,11 @@ def __repr__(self) -> str: if self.lower is None or self.upper is None: repr_str.write( - "%s, Type: NormalInteger, Mu: %s Sigma: %s, Default: %s" - % (self.name, repr(self.mu), repr(self.sigma), repr(self.default_value)), + f"{self.name}, Type: NormalInteger, Mu: {self.mu!r} Sigma: {self.sigma!r}, Default: {self.default_value!r}", ) else: repr_str.write( - "%s, Type: NormalInteger, Mu: %s Sigma: %s, Range: [%s, %s], Default: %s" - % ( + "{}, Type: NormalInteger, Mu: {} Sigma: {}, Range: [{}, {}], Default: {}".format( self.name, repr(self.mu), repr(self.sigma), @@ -234,7 +232,7 @@ def is_legal(self, value: int) -> bool: def is_legal_vector(self, value) -> int: return isinstance(value, (float, int)) - def check_default(self, default_value: int) -> int: + def check_default(self, default_value: int | None) -> int: if default_value is None: if self.log: return self._transform_scalar(self.mu) @@ -249,15 +247,14 @@ def check_default(self, default_value: int) -> int: def _sample( self, rs: np.random.RandomState, - size: Optional[int] = None, - ) -> Union[np.ndarray, float]: + size: int | None = None, + ) -> np.ndarray | float: value = self.nfhp._sample(rs, size=size) # Map all floats which belong to the same integer value to the same # float value by first transforming it to an integer and then # transforming it back to a float between zero and one value = self._transform(value) - value = self._inverse_transform(value) - return value + return self._inverse_transform(value) def _transform_vector(self, vector) -> np.ndarray: vector = self.nfhp._transform_vector(vector) @@ -269,8 +266,8 @@ def _transform_scalar(self, scalar: float) -> float: def _inverse_transform( self, - vector: Union[np.ndarray, float, int], - ) -> Union[np.ndarray, float]: + vector: np.ndarray | float | int, + ) -> np.ndarray | float: return self.nfhp._inverse_transform(vector) def has_neighbors(self) -> bool: @@ -278,7 +275,7 @@ def has_neighbors(self) -> bool: def get_neighbors( self, - value: Union[int, float], + value: int | float, rs: np.random.RandomState, number: int = 4, transform: bool = False, @@ -377,7 +374,7 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: distributions, only normal distributions (as the inverse_transform in the pdf method handles these). Optimally, an IntegerHyperparameter should have a corresponding float, which can be utlized for the calls - to the probability density function (see e.g. NormalIntegerHyperparameter) + to the probability density function (see e.g. NormalIntegerHyperparameter). Parameters ---------- @@ -401,8 +398,4 @@ def get_size(self) -> float: if self.lower is None: return np.inf else: - if self.q is None: - q = 1 - else: - q = self.q return np.rint((self.upper - self.lower) / self.q) + 1 diff --git a/ConfigSpace/hyperparameters/numerical.py b/ConfigSpace/hyperparameters/numerical.py index b51e9518..9875ebad 100644 --- a/ConfigSpace/hyperparameters/numerical.py +++ b/ConfigSpace/hyperparameters/numerical.py @@ -1,14 +1,14 @@ from __future__ import annotations -from typing import Any, Optional, Union +from typing import Any import numpy as np -from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter +from ConfigSpace.hyperparameters.hyperparameter import Comparison, Hyperparameter class NumericalHyperparameter(Hyperparameter): - def __init__(self, name: str, default_value: Any, meta: Optional[dict]) -> None: + def __init__(self, name: str, default_value: Any, meta: dict | None) -> None: super().__init__(name, meta) self.default_value = default_value @@ -18,22 +18,22 @@ def has_neighbors(self) -> bool: def get_num_neighbors(self, value=None) -> float: return np.inf - def compare(self, value: Union[int, float, str], value2: Union[int, float, str]) -> int: + def compare(self, value: int | float, value2: int | float) -> Comparison: if value < value2: - return -1 - elif value > value2: - return 1 - elif value == value2: - return 0 + return Comparison.LESS_THAN + if value > value2: + return Comparison.GREATER_THAN + + return Comparison.EQUAL - def compare_vector(self, value, value2) -> int: + def compare_vector(self, value: float, value2: float) -> Comparison: if value < value2: - return -1 + return Comparison.LESS_THAN if value > value2: - return 1 + return Comparison.GREATER_THAN - return 0 + return Comparison.EQUAL def allow_greater_less_comparison(self) -> bool: return True diff --git a/ConfigSpace/hyperparameters/ordinal.py b/ConfigSpace/hyperparameters/ordinal.py index 6dbf9c83..1bd0ad69 100644 --- a/ConfigSpace/hyperparameters/ordinal.py +++ b/ConfigSpace/hyperparameters/ordinal.py @@ -3,20 +3,20 @@ import copy import io from collections import OrderedDict -from typing import Any, Optional, Union +from typing import Any import numpy as np -from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter +from ConfigSpace.hyperparameters.hyperparameter import Comparison, Hyperparameter class OrdinalHyperparameter(Hyperparameter): def __init__( self, name: str, - sequence: Union[list[Union[float, int, str]], tuple[Union[float, int, str]]], - default_value: Union[str, int, float, None] = None, - meta: Optional[dict] = None, + sequence: list[float | int | str] | tuple[float | int | str], + default_value: str | int | float | None = None, + meta: dict | None = None, ) -> None: """ An ordinal hyperparameter. @@ -49,7 +49,7 @@ def __init__( # Since the sequence can consist of elements from different types, # they are stored into a dictionary in order to handle them as a # numeric sequence according to their order/position. - super(OrdinalHyperparameter, self).__init__(name, meta) + super().__init__(name, meta) if len(sequence) > len(set(sequence)): raise ValueError( "Ordinal Hyperparameter Sequence %s contain duplicate values." % sequence, @@ -59,19 +59,13 @@ def __init__( self.sequence_vector = list(range(self.num_elements)) self.default_value = self.check_default(default_value) self.normalized_default_value = self._inverse_transform(self.default_value) - self.value_dict = OrderedDict() # type: OrderedDict[Union[int, float, str], int] - counter = 0 - for element in self.sequence: - self.value_dict[element] = counter - counter += 1 + self.value_dict = {e: i for i, e in enumerate(self.sequence)} def __hash__(self): return hash((self.name, self.sequence)) def __repr__(self) -> str: - """ - write out the parameter definition - """ + """Write out the parameter definition.""" repr_str = io.StringIO() repr_str.write("%s, Type: Ordinal, Sequence: {" % (self.name)) for idx, seq in enumerate(self.sequence): @@ -85,9 +79,7 @@ def __repr__(self) -> str: return repr_str.getvalue() def __eq__(self, other: Any) -> bool: - """ - This method implements a comparison between self and another - object. + """Comparison between self and another object. Additionally, it defines the __ne__() as stated in the documentation from python: @@ -114,26 +106,25 @@ def __copy__(self): meta=self.meta, ) - def compare(self, value: Union[int, float, str], value2: Union[int, float, str]) -> int: + def compare(self, value: int | float | str, value2: int | float | str) -> Comparison: if self.value_dict[value] < self.value_dict[value2]: - return -1 - elif self.value_dict[value] > self.value_dict[value2]: - return 1 - elif self.value_dict[value] == self.value_dict[value2]: - return 0 + return Comparison.LESS_THAN + + if self.value_dict[value] > self.value_dict[value2]: + return Comparison.GREATER_THAN - def compare_vector(self, value, value2) -> int: + return Comparison.EQUAL + + def compare_vector(self, value, value2) -> Comparison: if value < value2: - return -1 - elif value > value2: - return 1 - elif value == value2: - return 0 + return Comparison.LESS_THAN + if value > value2: + return Comparison.GREATER_THAN - def is_legal(self, value: Union[int, float, str]) -> bool: - """ - check if a certain value is represented in the sequence - """ + return Comparison.EQUAL + + def is_legal(self, value: int | float | str) -> bool: + """Check if a certain value is represented in the sequence.""" return value in self.sequence def is_legal_vector(self, value) -> int: @@ -141,8 +132,8 @@ def is_legal_vector(self, value) -> int: def check_default( self, - default_value: Optional[Union[int, float, str]], - ) -> Union[int, float, str]: + default_value: int | float | str | None, + ) -> int | float | str: """ check if given default value is represented in the sequence. If there's no default value we simply choose the @@ -164,11 +155,11 @@ def _transform_vector(self, vector: np.ndarray) -> np.ndarray: raise ValueError( "Can only index the choices of the ordinal " - "hyperparameter %s with an integer, but provided " - "the following float: %f" % (self, vector), + f"hyperparameter {self} with an integer, but provided " + f"the following float: {vector:f}", ) - def _transform_scalar(self, scalar: Union[float, int]) -> Union[float, int, str]: + def _transform_scalar(self, scalar: float | int) -> float | int | str: if scalar != scalar: raise ValueError("Number %s is NaN" % scalar) @@ -177,14 +168,14 @@ def _transform_scalar(self, scalar: Union[float, int]) -> Union[float, int, str] raise ValueError( "Can only index the choices of the ordinal " - "hyperparameter %s with an integer, but provided " - "the following float: %f" % (self, scalar), + f"hyperparameter {self} with an integer, but provided " + f"the following float: {scalar:f}", ) def _transform( self, - vector: Union[np.ndarray, float, int], - ) -> Optional[Union[np.ndarray, float, int]]: + vector: np.ndarray | float | int, + ) -> np.ndarray | float | int | None: try: if isinstance(vector, np.ndarray): return self._transform_vector(vector) @@ -194,8 +185,8 @@ def _transform( def _inverse_transform( self, - vector: Optional[Union[np.ndarray, list, int, str, float]], - ) -> Union[float, list[int], list[str], list[float]]: + vector: np.ndarray | list | int | str | float | None, + ) -> float | list[int] | list[str] | list[float]: if vector is None: return np.NaN return self.sequence.index(vector) @@ -207,62 +198,49 @@ def get_seq_order(self) -> np.ndarray: """ return np.arange(0, self.num_elements) - def get_order(self, value: Optional[Union[int, str, float]]) -> int: - """ - return the seuence position/order of a certain value from the sequence - """ + def get_order(self, value: int | str | float | None) -> int: + """Return the seuence position/order of a certain value from the sequence.""" return self.value_dict[value] - def get_value(self, idx: int) -> Union[int, str, float]: - """ - return the sequence value of a given order/position - """ + def get_value(self, idx: int) -> int | str | float: + """Return the sequence value of a given order/position.""" return list(self.value_dict.keys())[list(self.value_dict.values()).index(idx)] - def check_order(self, val1: Union[int, str, float], val2: Union[int, str, float]) -> bool: - """ - check whether value1 is smaller than value2. - """ + def check_order(self, val1: int | str | float, val2: int | str | float) -> bool: + """Check whether value1 is smaller than value2.""" idx1 = self.get_order(val1) idx2 = self.get_order(val2) - if idx1 < idx2: - return True - else: - return False + return idx1 < idx2 - def _sample(self, rs: np.random.RandomState, size: Optional[int] = None) -> int: - """ - return a random sample from our sequence as order/position index - """ + def _sample(self, rs: np.random.RandomState, size: int | None = None) -> int: + """Return a random sample from our sequence as order/position index.""" return rs.randint(0, self.num_elements, size=size) def has_neighbors(self) -> bool: """ check if there are neighbors or we're only dealing with an - one-element sequence + one-element sequence. """ return len(self.sequence) > 1 - def get_num_neighbors(self, value: Union[int, float, str]) -> int: - """ - return the number of existing neighbors in the sequence - """ + def get_num_neighbors(self, value: int | float | str) -> int: + """Return the number of existing neighbors in the sequence.""" max_idx = len(self.sequence) - 1 # check if there is only one value if value == self.sequence[0] and value == self.sequence[max_idx]: return 0 - elif value == self.sequence[0] or value == self.sequence[max_idx]: + elif value in (self.sequence[0], self.sequence[max_idx]): return 1 else: return 2 def get_neighbors( self, - value: Union[int, str, float], + value: int | str | float, rs: None, number: int = 0, transform: bool = False, - ) -> list[Union[str, float, int]]: + ) -> list[str | float | int]: """ Return the neighbors of a given value. Value must be in vector form. Ordinal name will not work. diff --git a/ConfigSpace/hyperparameters/uniform_float.py b/ConfigSpace/hyperparameters/uniform_float.py index c0777a3e..5eda0f1e 100644 --- a/ConfigSpace/hyperparameters/uniform_float.py +++ b/ConfigSpace/hyperparameters/uniform_float.py @@ -2,24 +2,26 @@ import io import math -from typing import Any, Optional, Union +from typing import TYPE_CHECKING, Any import numpy as np from ConfigSpace.hyperparameters.float_hyperparameter import FloatHyperparameter -from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter + +if TYPE_CHECKING: + from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter class UniformFloatHyperparameter(FloatHyperparameter): def __init__( self, name: str, - lower: Union[int, float], - upper: Union[int, float], - default_value: Union[int, float, None] = None, - q: Union[int, float, None] = None, + lower: int | float, + upper: int | float, + default_value: int | float | None = None, + q: int | float | None = None, log: bool = False, - meta: Optional[dict] = None, + meta: dict | None = None, ) -> None: """ A uniformly distributed float hyperparameter. @@ -52,7 +54,7 @@ def __init__( Not used by the configuration space. """ default_value = None if default_value is None else float(default_value) - super(UniformFloatHyperparameter, self).__init__(name, default_value, meta) + super().__init__(name, default_value, meta) self.lower = float(lower) self.upper = float(upper) self.q = float(q) if q is not None else None @@ -60,13 +62,13 @@ def __init__( if self.lower >= self.upper: raise ValueError( - "Upper bound %f must be larger than lower bound " - "%f for hyperparameter %s" % (self.upper, self.lower, name), + f"Upper bound {self.upper:f} must be larger than lower bound " + f"{self.lower:f} for hyperparameter {name}", ) elif log and self.lower <= 0: raise ValueError( - "Negative lower bound (%f) for log-scale " - "hyperparameter %s is forbidden." % (self.lower, name), + f"Negative lower bound ({self.lower:f}) for log-scale " + f"hyperparameter {name} is forbidden.", ) self.default_value = self.check_default(default_value) @@ -93,8 +95,7 @@ def __init__( # Out[13]: 0.1999999999999998 if np.round((self.upper - self.lower) % self.q, 10) not in (0, self.q): raise ValueError( - "Upper bound (%f) - lower bound (%f) must be a multiple of q (%f)" - % (self.upper, self.lower, self.q), + f"Upper bound ({self.upper:f}) - lower bound ({self.lower:f}) must be a multiple of q ({self.q:f})", ) self.normalized_default_value = self._inverse_transform(self.default_value) @@ -102,8 +103,7 @@ def __init__( def __repr__(self) -> str: repr_str = io.StringIO() repr_str.write( - "%s, Type: UniformFloat, Range: [%s, %s], Default: %s" - % (self.name, repr(self.lower), repr(self.upper), repr(self.default_value)), + f"{self.name}, Type: UniformFloat, Range: [{self.lower!r}, {self.upper!r}], Default: {self.default_value!r}", ) if self.log: repr_str.write(", on log-scale") @@ -112,21 +112,15 @@ def __repr__(self) -> str: repr_str.seek(0) return repr_str.getvalue() - def is_legal(self, value: Union[float]) -> bool: - if not (isinstance(value, (float, int))): - return False - elif self.upper >= value >= self.lower: - return True - else: + def is_legal(self, value: float) -> bool: + if not isinstance(value, (float, int)): return False + return self.upper >= value >= self.lower def is_legal_vector(self, value) -> bool: - if 1.0 >= value >= 0.0: - return True - else: - return False + return 1.0 >= value >= 0.0 - def check_default(self, default_value: Union[float, int, None]) -> float: + def check_default(self, default_value: float | int | None) -> float: if default_value is None: if self.log: default_value = float(np.exp((np.log(self.lower) + np.log(self.upper)) / 2.0)) @@ -143,16 +137,18 @@ def to_integer(self) -> UniformIntegerHyperparameter: # TODO check if conversion makes sense at all (at least two integer values possible!) # todo check if params should be converted to int while class initialization # or inside class itself + from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter + return UniformIntegerHyperparameter( name=self.name, lower=int(np.ceil(self.lower)), upper=int(np.floor(self.upper)), default_value=int(np.rint(self.default_value)), - q=int(np.rint(self.q)), + q=int(np.rint(self.q)) if self.q is not None else None, log=self.log, ) - def _sample(self, rs: np.random, size: Optional[int] = None) -> Union[float, np.ndarray]: + def _sample(self, rs: np.random, size: int | None = None) -> float | np.ndarray: return rs.uniform(size=size) def _transform_vector(self, vector: np.ndarray) -> np.ndarray: @@ -177,18 +173,19 @@ def _transform_scalar(self, scalar: float) -> float: scalar = np.round((scalar - self.lower) / self.q) * self.q + self.lower scalar = min(scalar, self.upper) scalar = max(scalar, self.lower) - scalar = min(self.upper, max(self.lower, scalar)) - return scalar + return min(self.upper, max(self.lower, scalar)) - def _inverse_transform(self, vector: Union[np.ndarray, None]) -> Union[np.ndarray, float, int]: + def _inverse_transform( + self, + vector: np.ndarray | float | int | None, + ) -> np.ndarray | float: if vector is None: return np.NaN if self.log: vector = np.log(vector) vector = (vector - self._lower) / (self._upper - self._lower) vector = np.minimum(1.0, vector) - vector = np.maximum(0.0, vector) - return vector + return np.maximum(0.0, vector) def get_neighbors( self, diff --git a/ConfigSpace/hyperparameters/uniform_integer.py b/ConfigSpace/hyperparameters/uniform_integer.py index 86b8e654..be06142e 100644 --- a/ConfigSpace/hyperparameters/uniform_integer.py +++ b/ConfigSpace/hyperparameters/uniform_integer.py @@ -1,7 +1,6 @@ from __future__ import annotations import io -import warnings import numpy as np @@ -14,8 +13,8 @@ class UniformIntegerHyperparameter(IntegerHyperparameter): def __init__( self, name: str, - lower: int, - upper: int, + lower: int | float, + upper: int | float, default_value: int | None = None, q: int | None = None, log: bool = False, @@ -51,46 +50,45 @@ def __init__( Field for holding meta data provided by the user. Not used by the configuration space. """ + self.log = bool(log) self.lower = self.check_int(lower, "lower") self.upper = self.check_int(upper, "upper") - if default_value is not None: - default_value = self.check_int(default_value, name) - else: - default_value = self.check_default(default_value) - - # NOTE: Placed after the default value check to ensure it's set and not None - super().__init__(name, default_value, meta) - + self.q = None if q is not None: if q < 1: - warnings.warn( + raise ValueError( "Setting quantization < 1 for Integer " - "Hyperparameter '%s' has no effect." % name, + f"Hyperparameter {name} has no effect." + ) + + self.q = self.check_int(q, "q") + if (self.upper - self.lower) % self.q != 0: + raise ValueError( + "Upper bound (%d) - lower bound (%d) must be a multiple of q (%d)" + % (self.upper, self.lower, self.q), ) - self.q = None - else: - self.q = self.check_int(q, "q") - if (self.upper - self.lower) % self.q != 0: - raise ValueError( - "Upper bound (%d) - lower bound (%d) must be a multiple of q (%d)" - % (self.upper, self.lower, self.q), - ) - else: - self.q = None - self.log = bool(log) if self.lower >= self.upper: raise ValueError( "Upper bound %d must be larger than lower bound " "%d for hyperparameter %s" % (self.lower, self.upper, name), ) - elif log and self.lower <= 0: + if log and self.lower <= 0: raise ValueError( "Negative lower bound (%d) for log-scale " "hyperparameter %s is forbidden." % (self.lower, name), ) + # Requires `log` to be set first + if default_value is not None: + default_value = self.check_int(default_value, name) + else: + default_value = self.check_default(default_value) + + # NOTE: Placed after the default value check to ensure it's set and not None + super().__init__(name, default_value, meta) + self.ufhp = UniformFloatHyperparameter( self.name, self.lower - 0.49999, @@ -157,6 +155,7 @@ def is_legal_vector(self, value) -> int: return 1.0 >= value >= 0.0 def check_default(self, default_value: int | float | None) -> int: + # Doesn't seem to quantize with q? if default_value is None: if self.log: default_value = np.exp((np.log(self.lower) + np.log(self.upper)) / 2.0) diff --git a/ConfigSpace/read_and_write/json.py b/ConfigSpace/read_and_write/json.py index 09815531..a922042e 100644 --- a/ConfigSpace/read_and_write/json.py +++ b/ConfigSpace/read_and_write/json.py @@ -347,8 +347,8 @@ def write(configuration_space: ConfigurationSpace, indent: int = 2) -> str: """ if not isinstance(configuration_space, ConfigurationSpace): raise TypeError( - "pcs_parser.write expects an instance of {}, " - "you provided '{}'".format(ConfigurationSpace, type(configuration_space)), + f"pcs_parser.write expects an instance of {ConfigurationSpace}, " + f"you provided '{type(configuration_space)}'", ) hyperparameters = [] @@ -378,10 +378,7 @@ def write(configuration_space: ConfigurationSpace, indent: int = 2) -> str: hyperparameters.append(_build_ordinal(hyperparameter)) else: raise TypeError( - "Unknown type: {} ({})".format( - type(hyperparameter), - hyperparameter, - ), + f"Unknown type: {type(hyperparameter)} ({hyperparameter})", ) for condition in configuration_space.get_conditions(): diff --git a/ConfigSpace/read_and_write/pcs.py b/ConfigSpace/read_and_write/pcs.py index 45c38e49..b30b25fe 100644 --- a/ConfigSpace/read_and_write/pcs.py +++ b/ConfigSpace/read_and_write/pcs.py @@ -153,7 +153,7 @@ def build_condition(condition: ConditionComponent) -> str: if not isinstance(condition, ConditionComponent): raise TypeError( "build_condition must be called with an instance of " - "'{}', got '{}'".format(ConditionComponent, type(condition)), + f"'{ConditionComponent}', got '{type(condition)}'", ) # Check if SMAC can handle the condition @@ -184,7 +184,7 @@ def build_forbidden(clause: AbstractForbiddenComponent) -> str: if not isinstance(clause, AbstractForbiddenComponent): raise TypeError( "build_forbidden must be called with an instance of " - "'{}', got '{}'".format(AbstractForbiddenComponent, type(clause)), + f"'{AbstractForbiddenComponent}', got '{type(clause)}'", ) if not isinstance(clause, (ForbiddenEqualsClause, ForbiddenAndConjunction)): diff --git a/ConfigSpace/read_and_write/pcs_new.py b/ConfigSpace/read_and_write/pcs_new.py index ee54eb3a..c22f2302 100644 --- a/ConfigSpace/read_and_write/pcs_new.py +++ b/ConfigSpace/read_and_write/pcs_new.py @@ -699,8 +699,8 @@ def write(configuration_space: ConfigurationSpace) -> str: """ if not isinstance(configuration_space, ConfigurationSpace): raise TypeError( - "pcs_parser.write expects an instance of {}, " - "you provided '{}'".format(ConfigurationSpace, type(configuration_space)), + f"pcs_parser.write expects an instance of {ConfigurationSpace}, " + f"you provided '{type(configuration_space)}'", ) param_lines = StringIO() diff --git a/ConfigSpace/util.py b/ConfigSpace/util.py index 120bfce3..208ff1bb 100644 --- a/ConfigSpace/util.py +++ b/ConfigSpace/util.py @@ -441,8 +441,8 @@ def fix_type_from_candidates(value: Any, candidates: list[Any]) -> Any: result = [c for c in candidates if str(value) == str(c)] if len(result) != 1: raise ValueError( - "Parameter value {} cannot be matched to candidates {}. " - "Either none or too many matching candidates.".format(str(value), candidates), + f"Parameter value {value!s} cannot be matched to candidates {candidates}. " + "Either none or too many matching candidates.", ) return result[0] @@ -658,7 +658,7 @@ def get_cartesian_product(value_sets: list[tuple], hp_names: list[str]) -> list[ while len(unchecked_grid_pts) > 0: try: - grid_point = Configuration(configuration_space, unchecked_grid_pts[0]) + grid_point = Configuration(configuration_space, values=unchecked_grid_pts[0]) checked_grid_pts.append(grid_point) # When creating a configuration that violates a forbidden clause, simply skip it @@ -673,9 +673,7 @@ def get_cartesian_product(value_sets: list[tuple], hp_names: list[str]) -> list[ # "for" loop over currently active HP names for hp_name in unchecked_grid_pts[0]: - value_sets.append( - (unchecked_grid_pts[0][hp_name],), - ) + value_sets.append((unchecked_grid_pts[0][hp_name],)) hp_names.append(hp_name) # Checks if the conditionally dependent children of already active # HPs are now active @@ -694,12 +692,13 @@ def get_cartesian_product(value_sets: list[tuple], hp_names: list[str]) -> list[ for hp_name in new_active_hp_names: value_sets.append(get_value_set(num_steps_dict, hp_name)) hp_names.append(hp_name) + # this check might not be needed, as there is always going to be a new # active HP when in this except block? if len(new_active_hp_names) <= 0: raise RuntimeError( "Unexpected error: There should have been a newly activated hyperparameter" - f" for the current configuration values: {str(unchecked_grid_pts[0])}. " + f" for the current configuration values: {unchecked_grid_pts[0]!s}. " "Please contact the developers with the code you ran and the stack trace.", ) from None diff --git a/test/test_conditions.py b/test/test_conditions.py index ede9ff1f..cb3ba58b 100644 --- a/test/test_conditions.py +++ b/test/test_conditions.py @@ -60,10 +60,6 @@ def test_equals_condition(): assert cond.vector_value == cond_.vector_value # Test invalid conditions: - with pytest.raises(TypeError): - EqualsCondition(hp2, "parent", 0) - with pytest.raises(TypeError): - EqualsCondition("child", hp1, 0) with pytest.raises(ValueError): EqualsCondition(hp1, hp1, 0) diff --git a/test/test_configuration_space.py b/test/test_configuration_space.py index 0152252e..3ae3d027 100644 --- a/test/test_configuration_space.py +++ b/test/test_configuration_space.py @@ -28,7 +28,6 @@ from __future__ import annotations import json -import unittest from collections import OrderedDict from itertools import product @@ -83,10 +82,8 @@ def test_add_hyperparameter(): def test_add_non_hyperparameter(): cs = ConfigurationSpace() - non_hp = unittest.TestSuite() - with pytest.raises(TypeError): - cs.add_hyperparameter(non_hp) + cs.add_hyperparameter(object()) # type: ignore def test_add_hyperparameters_with_equal_names(): @@ -123,9 +120,8 @@ def test_meta_data_stored(): def test_add_non_condition(): cs = ConfigurationSpace() - non_cond = unittest.TestSuite() with pytest.raises(TypeError): - cs.add_condition(non_cond) + cs.add_condition(object()) # type: ignore def test_hyperparameters_with_valid_condition(): @@ -684,7 +680,7 @@ def test_repr(): cs1.add_condition(cond1) retval = cs1.__str__() assert ( - f"Configuration space object:\n Hyperparameters:\n {hp2!s}\n {hp1!s}\n Conditions:\n {cond2!s}\n" + f"Configuration space object:\n Hyperparameters:\n {hp2}\n {hp1}\n Conditions:\n {cond1}\n" == retval ) @@ -879,25 +875,27 @@ def test_remove_hyperparameter_priors(): beta = BetaFloatHyperparameter("beta", alpha=8, beta=2, lower=-1, upper=11) norm = NormalIntegerHyperparameter("norm", mu=5, sigma=4, lower=1, upper=15) cs.add_hyperparameters([integer, cat, beta, norm]) + cat_default = cat.default_value norm_default = norm.default_value beta_default = beta.default_value # add some conditions, to test that remove_parameter_priors keeps the forbiddensdef test_remove_hyp - cond_1 = EqualsCondition(norm, cat, 2) cond_2 = OrConjunction(EqualsCondition(beta, cat, 0), EqualsCondition(beta, cat, 1)) cond_3 = OrConjunction( + EqualsCondition(norm, cat, 2), EqualsCondition(norm, integer, 1), EqualsCondition(norm, integer, 3), EqualsCondition(norm, integer, 5), ) - cs.add_conditions([cond_1, cond_2, cond_3]) + cs.add_conditions([cond_2, cond_3]) # add some forbidden clauses too, to test that remove_parameter_priors keeps the forbiddens forbidden_clause_a = ForbiddenEqualsClause(cat, 0) forbidden_clause_c = ForbiddenEqualsClause(integer, 3) forbidden_clause_d = ForbiddenAndConjunction(forbidden_clause_a, forbidden_clause_c) cs.add_forbidden_clauses([forbidden_clause_c, forbidden_clause_d]) + uniform_cs = cs.remove_hyperparameter_priors() expected_cs = ConfigurationSpace() @@ -919,17 +917,17 @@ def test_remove_hyperparameter_priors(): expected_cs.add_hyperparameters([unif_integer, unif_cat, unif_beta, unif_norm]) # add some conditions, to test that remove_parameter_priors keeps the forbiddens - cond_1 = EqualsCondition(unif_norm, unif_cat, 2) cond_2 = OrConjunction( EqualsCondition(unif_beta, unif_cat, 0), EqualsCondition(unif_beta, unif_cat, 1), ) cond_3 = OrConjunction( + EqualsCondition(unif_norm, unif_cat, 2), EqualsCondition(unif_norm, unif_integer, 1), EqualsCondition(unif_norm, unif_integer, 3), EqualsCondition(unif_norm, unif_integer, 5), ) - expected_cs.add_conditions([cond_1, cond_2, cond_3]) + expected_cs.add_conditions([cond_2, cond_3]) # add some forbidden clauses too, to test that remove_parameter_priors keeps the forbiddens forbidden_clause_a = ForbiddenEqualsClause(unif_cat, 0) @@ -1114,7 +1112,7 @@ def test_uniformfloat_transform(): assert values_dict == saved_value -def test_setitem(self): +def test_setitem(): """Checks overriding a sampled configuration.""" pcs = ConfigurationSpace() pcs.add_hyperparameter(UniformIntegerHyperparameter("x0", 1, 5, default_value=1)) @@ -1133,15 +1131,15 @@ def test_setitem(self): conf = pcs.get_default_configuration() # failed because it's a invalid configuration - with self.assertRaises(IllegalValueError): + with pytest.raises(IllegalValueError): conf["x0"] = 0 # failed because the variable didn't exists - with self.assertRaises(HyperparameterNotFoundError): + with pytest.raises(HyperparameterNotFoundError): conf["x_0"] = 1 # failed because forbidden clause is violated - with self.assertRaises(ForbiddenValueError): + with pytest.raises(ForbiddenValueError): conf["x3"] = 2 assert conf["x3"] == 1 @@ -1167,15 +1165,15 @@ def test_setitem(self): assert x1_old != x1_new pcs._check_configuration_rigorous(conf) - with self.assertRaises(KeyError): + with pytest.raises(KeyError): conf["x2"] -def test_setting_illegal_value(self): +def test_setting_illegal_value(): cs = ConfigurationSpace() cs.add_hyperparameter(UniformFloatHyperparameter("x", 0, 1)) configuration = {"x": 2} - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Configuration(cs, values=configuration) diff --git a/test/test_hyperparameters.py b/test/test_hyperparameters.py index ad4bef06..f5579cb0 100644 --- a/test/test_hyperparameters.py +++ b/test/test_hyperparameters.py @@ -118,13 +118,7 @@ def test_constant_pdf(): assert tuple(c1.pdf(array_2)) == tuple(np.array([0.0, 0.0])) assert tuple(c1.pdf(array_3)) == tuple(np.array([1.0, 0.0])) - # pdf must take a numpy array - with pytest.raises(TypeError): - c1.pdf(0.2) - with pytest.raises(TypeError): - c1.pdf("pdf") - - # and it must be one-dimensional + # it must be one-dimensional with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): c1.pdf(wrong_shape_1) with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): @@ -160,12 +154,6 @@ def test_constant__pdf(): assert tuple(c1._pdf(array_2)) == tuple(np.array([0.0, 0.0])) assert tuple(c1._pdf(array_3)) == tuple(np.array([1.0, 0.0])) - # pdf must take a numpy array - with pytest.raises(TypeError): - c1._pdf(0.2) - with pytest.raises(TypeError): - c1._pdf("pdf") - # Simply check that it runs, since _pdf does not restrict shape (only public method does) c1._pdf(accepted_shape_1) c1._pdf(accepted_shape_2) @@ -246,17 +234,29 @@ def test_uniformfloat(): def test_uniformfloat_to_integer(): - f1 = UniformFloatHyperparameter("param", 1, 10, q=0.1, log=True) - with pytest.warns( - UserWarning, - match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", - ): - f2 = f1.to_integer() + f1 = UniformFloatHyperparameter("param", 1, 10, log=True) + f2 = f1.to_integer() # TODO is this a useful rounding? # TODO should there be any rounding, if e.g. lower=0.1 assert str(f2) == "param, Type: UniformInteger, Range: [1, 10], Default: 3, on log-scale" +def test_uniformfloat_illegal_bounds(): + with pytest.raises( + ValueError, + match=r"Negative lower bound \(0.000000\) for log-scale hyperparameter " + r"param is forbidden.", + ): + _ = UniformFloatHyperparameter("param", 0, 10, q=0.1, log=True) + + with pytest.raises( + ValueError, + match="Upper bound 0.000000 must be larger than lower bound " + "1.000000 for hyperparameter param", + ): + _ = UniformFloatHyperparameter("param", 1, 0) + + def test_uniformfloat_is_legal(): lower = 0.1 upper = 10 @@ -276,22 +276,10 @@ def test_uniformfloat_is_legal(): assert f1.is_legal_vector(0.3) assert not f1.is_legal_vector(-0.1) assert not f1.is_legal_vector(1.1) - with pytest.raises(TypeError): - f1.is_legal_vector("Hahaha") - -def test_uniformfloat_illegal_bounds(): - with pytest.raises( - ValueError, - match=r"Negative lower bound \(0.000000\) for log-scale hyperparameter " r"param is forbidden.", - ): - _ = UniformFloatHyperparameter("param", 0, 10, q=0.1, log=True) - with pytest.raises( - ValueError, - match="Upper bound 0.000000 must be larger than lower bound " "1.000000 for hyperparameter param", - ): - _ = UniformFloatHyperparameter("param", 1, 0) + with pytest.raises(TypeError): + assert not f1.is_legal_vector("Hahaha") def test_uniformfloat_pdf(): @@ -310,9 +298,9 @@ def test_uniformfloat_pdf(): wrong_shape_3 = np.array([3, 5, 7]).reshape(-1, 1) assert c1.pdf(point_1)[0] == pytest.approx(0.1) - assert c2.pdf(point_2)[0] == pytest.approx(4.539992976248485e-05) + assert c2.pdf(point_2)[0] == pytest.approx(4.539992976248485e-05, abs=1e-3) assert c1.pdf(point_1)[0] == pytest.approx(0.1) - assert c2.pdf(point_2)[0] == pytest.approx(4.539992976248485e-05) + assert c2.pdf(point_2)[0] == pytest.approx(4.539992976248485e-05, abs=1e-3) assert c3.pdf(point_3)[0] == pytest.approx(2.0) # TODO - change this once the is_legal support is there @@ -320,7 +308,7 @@ def test_uniformfloat_pdf(): # since inverse_transform pulls everything into range, # even points outside get evaluated in range assert c1.pdf(point_outside_range)[0] == pytest.approx(0.1) - assert c2.pdf(point_outside_range_log)[0] == pytest.approx(4.539992976248485e-05) + assert c2.pdf(point_outside_range_log)[0] == pytest.approx(4.539992976248485e-05, abs=1e-5) # this, however, is a negative value on a log param, which cannot be pulled into range with pytest.warns(RuntimeWarning, match="invalid value encountered in log"): @@ -341,13 +329,7 @@ def test_uniformfloat_pdf(): expected_log_results, ): assert res == pytest.approx(exp_res) - assert log_res == pytest.approx(exp_log_res) - - # pdf must take a numpy array - with pytest.raises(TypeError): - c1.pdf(0.2) - with pytest.raises(TypeError): - c1.pdf("pdf") + assert log_res == pytest.approx(exp_log_res, abs=1e-5) with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): c1.pdf(wrong_shape_1) @@ -372,10 +354,10 @@ def test_uniformfloat__pdf(): accepted_shape_2 = np.array([0.3, 0.5, 1.1]).reshape(1, -1) accepted_shape_3 = np.array([1.1, 0.5, 0.3]).reshape(-1, 1) + assert c1._pdf(point_1)[0] == pytest.approx(0.1, abs=1e-3) + assert c2._pdf(point_2)[0] == pytest.approx(4.539992976248485e-05, abs=1e-3) assert c1._pdf(point_1)[0] == pytest.approx(0.1) - assert c2._pdf(point_2)[0] == pytest.approx(4.539992976248485e-05) - assert c1._pdf(point_1)[0] == pytest.approx(0.1) - assert c2._pdf(point_2)[0] == pytest.approx(4.539992976248485e-05) + assert c2._pdf(point_2)[0] == pytest.approx(4.539992976248485e-05, abs=1e-3) assert c3._pdf(point_3)[0] == pytest.approx(2.0) # TODO - change this once the is_legal support is there @@ -399,14 +381,8 @@ def test_uniformfloat__pdf(): expected_results, expected_log_results, ): - assert res == pytest.approx(exp_res) - assert log_res == pytest.approx(exp_log_res) - - # pdf must take a numpy array - with pytest.raises(TypeError): - c1._pdf(0.2) - with pytest.raises(TypeError): - c1._pdf("pdf") + assert res == pytest.approx(exp_res, abs=1e-5) + assert log_res == pytest.approx(exp_log_res, abs=1e-5) # Simply check that it runs, since _pdf does not restrict shape (only public method does) c1._pdf(accepted_shape_1) @@ -419,7 +395,7 @@ def test_uniformfloat_get_max_density(): c2 = UniformFloatHyperparameter("logparam", lower=np.exp(0), upper=np.exp(10), log=True) c3 = UniformFloatHyperparameter("param", lower=0, upper=0.5) assert c1.get_max_density() == 0.1 - assert c2.get_max_density() == pytest.approx(4.539992976248485e-05) + assert c2.get_max_density() == pytest.approx(4.5401991009687765e-05) assert c3.get_max_density() == 2 @@ -608,8 +584,7 @@ def test_normalfloat_is_legal(): assert f1.is_legal_vector(0.3) assert f1.is_legal_vector(-0.1) assert f1.is_legal_vector(1.1) - with pytest.raises(TypeError): - f1.is_legal_vector("Hahaha") + assert not f1.is_legal_vector("Hahaha") f2 = NormalFloatHyperparameter("param", 5, 10, lower=0.1, upper=10, default_value=5.0) assert f2.is_legal(5.0) @@ -672,12 +647,6 @@ def test_normalfloat_pdf(): assert res == pytest.approx(exp_res) assert log_res == pytest.approx(exp_res) - # pdf must take a numpy array - with pytest.raises(TypeError): - c1.pdf(0.2) - with pytest.raises(TypeError): - c1.pdf("pdf") - c_nobounds = NormalFloatHyperparameter("param", mu=3, sigma=2) assert c_nobounds.pdf(np.array([2]))[0] == pytest.approx(0.17603266338214976) @@ -735,12 +704,6 @@ def test_normalfloat__pdf(): assert res == pytest.approx(exp_res) assert log_res == pytest.approx(exp_res) - # pdf must take a numpy array - with pytest.raises(TypeError): - c1.pdf(0.2) - with pytest.raises(TypeError): - c1.pdf("pdf") - c_nobounds = NormalFloatHyperparameter("param", mu=3, sigma=2) assert c_nobounds.pdf(np.array([2]))[0] == pytest.approx(0.17603266338214976) @@ -822,6 +785,7 @@ def test_betafloat(): "param", lower=1, upper=1000.0, + default_value=32.0, alpha=2.0, beta=2.0, log=True, @@ -831,6 +795,7 @@ def test_betafloat(): "param", lower=1, upper=1000.0, + default_value=32.0, alpha=2.0, beta=2.0, log=True, @@ -856,7 +821,7 @@ def test_betafloat(): assert f_meta.meta == META_DATA with pytest.raises( - UserWarning, + ValueError, match=( "Logscale and quantization together results in " "incorrect default values. We recommend specifying a default " @@ -990,6 +955,7 @@ def test_betafloat_default_value(): lower=1, upper=100000, alpha=2, + default_value=316, beta=2, q=1, log=True, @@ -1129,12 +1095,6 @@ def test_betafloat_pdf(): assert res == pytest.approx(exp_res) assert log_res == pytest.approx(exp_res) - # pdf must take a numpy array - with pytest.raises(TypeError): - c1.pdf(0.2) - with pytest.raises(TypeError): - c1.pdf("pdf") - with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): c1.pdf(wrong_shape_1) with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): @@ -1187,12 +1147,6 @@ def test_betafloat__pdf(): assert res == pytest.approx(exp_res) assert log_res == pytest.approx(exp_res) - # pdf must take a numpy array - with pytest.raises(TypeError): - c1.pdf(0.2) - with pytest.raises(TypeError): - c1.pdf("pdf") - # Simply check that it runs, since _pdf does not restrict shape (only public method does) c1._pdf(accepted_shape_1) c1._pdf(accepted_shape_2) @@ -1233,14 +1187,12 @@ def test_uniforminteger(): assert f1.normalized_default_value == pytest.approx((2.0 + 0.49999) / (5.49999 + 0.49999)) quantization_warning = ( - "Setting quantization < 1 for Integer Hyperparameter 'param' has no effect" + "Setting quantization < 1 for Integer Hyperparameter param has no effect" ) - with pytest.warns(UserWarning, match=quantization_warning): - f2 = UniformIntegerHyperparameter("param", 0, 10, q=0.1) - with pytest.warns(UserWarning, match=quantization_warning): - f2_ = UniformIntegerHyperparameter("param", 0, 10, q=0.1) - assert f2 == f2_ - assert str(f2) == "param, Type: UniformInteger, Range: [0, 10], Default: 5" + with pytest.raises(ValueError, match=quantization_warning): + _ = UniformIntegerHyperparameter("param", 0, 10, q=0.1) # type: ignore + with pytest.raises(ValueError, match=quantization_warning): + _ = UniformIntegerHyperparameter("param", 0, 10, q=0.1) # type: ignore f2_large_q = UniformIntegerHyperparameter("param", 0, 10, q=2) f2_large_q_ = UniformIntegerHyperparameter("param", 0, 10, q=2) @@ -1257,41 +1209,31 @@ def test_uniforminteger(): assert f4 == f4_ assert str(f4) == "param, Type: UniformInteger, Range: [1, 10], Default: 1, on log-scale" - with pytest.warns(UserWarning, match=quantization_warning): - f5 = UniformIntegerHyperparameter("param", 1, 10, default_value=1, q=0.1, log=True) - with pytest.warns(UserWarning, match=quantization_warning): - f5_ = UniformIntegerHyperparameter("param", 1, 10, default_value=1, q=0.1, log=True) - assert f5 == f5_ - assert str(f5) == "param, Type: UniformInteger, Range: [1, 10], Default: 1, on log-scale" - assert f1 != "UniformFloat" # test that meta-data is stored correctly - with pytest.warns(UserWarning, match=quantization_warning): - f_meta = UniformIntegerHyperparameter( - "param", - 1, - 10, - q=0.1, - log=True, - default_value=1, - meta=dict(META_DATA), - ) + f_meta = UniformIntegerHyperparameter( + "param", + 1, + 10, + log=True, + default_value=1, + meta=dict(META_DATA), + ) assert f_meta.meta == META_DATA assert f1.get_size() == 6 - assert f2.get_size() == 11 assert f2_large_q.get_size() == 6 assert f3.get_size() == 10 assert f4.get_size() == 10 - assert f5.get_size() == 10 def test_uniformint_legal_float_values(): n_iter = UniformIntegerHyperparameter("n_iter", 5.0, 1000.0, default_value=20.0) assert isinstance(n_iter.default_value, int) - with pytest.raises(ValueError, + with pytest.raises( + ValueError, match=r"For the Integer parameter n_iter, " r"the value must be an Integer, too." r" Right now it is a <(type|class) " @@ -1312,7 +1254,7 @@ def test_uniformint_illegal_bounds(): ValueError, match="Upper bound 1 must be larger than lower bound 0 for " "hyperparameter param", ): - _ = UniformIntegerHyperparameter( "param", 1, 0) + _ = UniformIntegerHyperparameter("param", 1, 0) def test_uniformint_pdf(): @@ -1367,12 +1309,6 @@ def test_uniformint_pdf(): assert res == pytest.approx(exp_res, abs=1e-5) assert log_res == pytest.approx(exp_res, abs=1e-5) - # pdf must take a numpy array - with pytest.raises(TypeError): - c1.pdf(0.2) - with pytest.raises(TypeError): - c1.pdf("pdf") - with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): c1.pdf(wrong_shape_1) with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): @@ -1422,12 +1358,6 @@ def test_uniformint__pdf(): assert res == pytest.approx(exp_res, abs=1e-5) assert log_res == pytest.approx(exp_log_res, abs=1e-5) - # pdf must take a numpy array - with pytest.raises(TypeError): - c1._pdf(0.2) - with pytest.raises(TypeError): - c1._pdf("pdf") - # Simply check that it runs, since _pdf does not restrict shape (only public method does) c1._pdf(accepted_shape_1) c1._pdf(accepted_shape_2) @@ -1586,8 +1516,7 @@ def test_normalint_is_legal(): assert f1.is_legal_vector(0.3) assert f1.is_legal_vector(-0.1) assert f1.is_legal_vector(1.1) - with pytest.raises(TypeError): - f1.is_legal_vector("Hahaha") + assert not f1.is_legal_vector("Hahaha") f2 = NormalIntegerHyperparameter("param", 5, 10, lower=1, upper=10, default_value=5) assert f2.is_legal(5) @@ -1647,12 +1576,6 @@ def test_normalint_pdf(): assert res == pytest.approx(exp_res) assert log_res == pytest.approx(exp_log_res) - # pdf must take a numpy array - with pytest.raises(TypeError): - c1.pdf(0.2) - with pytest.raises(TypeError): - c1.pdf("pdf") - c_nobounds = NormalFloatHyperparameter("param", mu=3, sigma=2) assert c_nobounds.pdf(np.array([2]))[0] == pytest.approx(0.17603266338214976) @@ -1700,12 +1623,6 @@ def test_normalint__pdf(): assert res == pytest.approx(exp_res) assert log_res == pytest.approx(exp_log_res) - # pdf must take a numpy array - with pytest.raises(TypeError): - c1.pdf(0.2) - with pytest.raises(TypeError): - c1.pdf("pdf") - c_nobounds = NormalFloatHyperparameter("param", mu=3, sigma=2) assert c_nobounds.pdf(np.array([2]))[0] == pytest.approx(0.17603266338214976) @@ -1963,16 +1880,19 @@ def test_betaint_legal_float_values(): ValueError, match="Illegal default value 0.5", ): - _ = BetaIntegerHyperparameter("param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.1, default_value=0.5) + _ = BetaIntegerHyperparameter( + "param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.1, default_value=0.5, + ) def test_betaint_to_uniform(): - with pytest.warns( - UserWarning, - match="Setting quantization < 1 for Integer " "Hyperparameter 'param' has no effect", + with pytest.raises( + ValueError, + match="Setting quantization < 1 for Integer Hyperparameter param has no effect", ): - f1 = BetaIntegerHyperparameter("param", lower=-30, upper=30, alpha=6.0, beta=2, q=0.1) + _ = BetaIntegerHyperparameter("param", lower=-30, upper=30, alpha=6.0, beta=2, q=0.1) + f1 = BetaIntegerHyperparameter("param", lower=-30, upper=30, alpha=6.0, beta=2) f1_expected = UniformIntegerHyperparameter("param", -30, 30, default_value=20) f1_actual = f1.to_uniform() assert f1_expected == f1_actual @@ -2030,12 +1950,6 @@ def test_betaint_pdf(): assert res == pytest.approx(exp_res, abs=1e-3) assert log_res == pytest.approx(exp_log_res, abs=1e-3) - # pdf must take a numpy array - with pytest.raises(TypeError): - c1.pdf(0.2) - with pytest.raises(TypeError): - c1.pdf("pdf") - with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): c1.pdf(wrong_shape_1) with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): @@ -2094,14 +2008,8 @@ def test_betaint__pdf(): expected_results, expected_results_log, ): - assert res == pytest.approx(exp_res) - assert log_res == pytest.approx(exp_log_res) - - # pdf must take a numpy array - with pytest.raises(TypeError): - c1._pdf(0.2) - with pytest.raises(TypeError): - c1._pdf("pdf") + assert res == pytest.approx(exp_res, abs=1e-5) + assert log_res == pytest.approx(exp_log_res, abs=1e-5) # Simply check that it runs, since _pdf does not restrict shape (only public method does) c1._pdf(accepted_shape_1) @@ -2251,8 +2159,7 @@ def test_categorical_is_legal(): assert f1.is_legal_vector(0) assert not f1.is_legal_vector(0.3) assert not f1.is_legal_vector(-0.1) - with pytest.raises(TypeError): - f1.is_legal_vector("Hahaha") + assert not f1.is_legal_vector("Hahaha") def test_categorical_choices(): @@ -2399,16 +2306,6 @@ def test_categorical_pdf(): assert c2.pdf(point_2)[0] == 0.0 assert c3.pdf(point_1)[0] == 0.25 - # pdf must take a numpy array - with pytest.raises(TypeError): - c1.pdf(0.2) - with pytest.raises(TypeError): - c1.pdf("pdf") - with pytest.raises(TypeError): - c1.pdf("one") - with pytest.raises(ValueError): - c1.pdf(np.array(["zero"])) - with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): c1.pdf(wrong_shape_1) with pytest.raises(ValueError, match="Method pdf expects a one-dimensional numpy array"): @@ -2442,16 +2339,6 @@ def test_categorical__pdf(): for res, exp_res in zip(nan_results, expected_results): assert res == exp_res - # pdf must take a numpy array - with pytest.raises(TypeError): - c1._pdf(0.2) - with pytest.raises(TypeError): - c1._pdf("pdf") - with pytest.raises(TypeError): - c1._pdf("one") - with pytest.raises(TypeError): - c1._pdf(np.array(["zero"])) - def test_categorical_get_max_density(): c1 = CategoricalHyperparameter("x1", choices=["one", "two", "three"], weights=[2, 1, 2]) @@ -2997,8 +2884,7 @@ def test_ordinal_is_legal(): assert f1.is_legal_vector(0) assert f1.is_legal_vector(3) assert not f1.is_legal_vector(-0.1) - with pytest.raises(TypeError): - f1.is_legal_vector("Hahaha") + assert not f1.is_legal_vector("Hahaha") def test_ordinal_check_order(): @@ -3066,13 +2952,6 @@ def test_ordinal_pdf(): for res, exp_res in zip(array_results, expected_results): assert res == exp_res - # pdf must take a numpy array - with pytest.raises(TypeError): - c1.pdf(0.2) - with pytest.raises(TypeError): - c1.pdf("pdf") - with pytest.raises(TypeError): - c1.pdf("one") with pytest.raises(ValueError): c1.pdf(np.array(["zero"])) @@ -3098,13 +2977,6 @@ def test_ordinal__pdf(): for res, exp_res in zip(array_results, expected_results): assert res == exp_res - # pdf must take a numpy array - with pytest.raises(TypeError): - c1._pdf(0.2) - with pytest.raises(TypeError): - c1._pdf("pdf") - with pytest.raises(TypeError): - c1._pdf("one") with pytest.raises(ValueError): c1._pdf(np.array(["zero"])) @@ -3126,7 +2998,9 @@ def test_rvs(): assert isinstance(f1.rvs(size=2), np.ndarray) assert f1.rvs(random_state=100) == pytest.approx(f1.rvs(random_state=100)) - assert f1.rvs(random_state=100) == pytest.approx(f1.rvs(random_state=np.random.RandomState(100))) + assert f1.rvs(random_state=100) == pytest.approx( + f1.rvs(random_state=np.random.RandomState(100)), + ) f1.rvs(random_state=np.random) f1.rvs(random_state=np.random.default_rng(1)) with pytest.raises(ValueError): From 2a2912083cea011df5613eedb40b66a5fb22e2f0 Mon Sep 17 00:00:00 2001 From: eddiebergman Date: Tue, 19 Dec 2023 14:58:30 +0100 Subject: [PATCH 003/104] Fixup tests --- ConfigSpace/configuration.py | 4 ++- ConfigSpace/configuration_space.py | 3 +-- ConfigSpace/hyperparameters/__init__.py | 3 +-- ConfigSpace/hyperparameters/hyperparameter.py | 1 - ConfigSpace/hyperparameters/normal_float.py | 5 +++- ConfigSpace/hyperparameters/ordinal.py | 1 - .../hyperparameters/uniform_integer.py | 26 ++++++++++--------- ConfigSpace/util.py | 4 +-- pyproject.toml | 1 + test/test_configspace_from_dict.py | 6 ----- 10 files changed, 26 insertions(+), 28 deletions(-) diff --git a/ConfigSpace/configuration.py b/ConfigSpace/configuration.py index 463c560c..a9450e25 100644 --- a/ConfigSpace/configuration.py +++ b/ConfigSpace/configuration.py @@ -8,11 +8,13 @@ from ConfigSpace import c_util from ConfigSpace.exceptions import HyperparameterNotFoundError, IllegalValueError from ConfigSpace.hyperparameters import FloatHyperparameter -from ConfigSpace.hyperparameters.hyperparameter import NotSet if TYPE_CHECKING: from ConfigSpace.configuration_space import ConfigurationSpace +NotSet = object() # Sentinal value for unset values + + class Configuration(Mapping[str, Any]): def __init__( self, diff --git a/ConfigSpace/configuration_space.py b/ConfigSpace/configuration_space.py index d121bb52..a95fb9cf 100644 --- a/ConfigSpace/configuration_space.py +++ b/ConfigSpace/configuration_space.py @@ -45,7 +45,7 @@ ConditionComponent, EqualsCondition, ) -from ConfigSpace.configuration import Configuration +from ConfigSpace.configuration import Configuration, NotSet from ConfigSpace.exceptions import ( ActiveHyperparameterNotSetError, AmbiguousConditionError, @@ -72,7 +72,6 @@ UniformFloatHyperparameter, UniformIntegerHyperparameter, ) -from ConfigSpace.hyperparameters.hyperparameter import NotSet _ROOT: Final = "__HPOlib_configuration_space_root__" diff --git a/ConfigSpace/hyperparameters/__init__.py b/ConfigSpace/hyperparameters/__init__.py index ef85f8b4..87e13f0a 100644 --- a/ConfigSpace/hyperparameters/__init__.py +++ b/ConfigSpace/hyperparameters/__init__.py @@ -3,7 +3,7 @@ from ConfigSpace.hyperparameters.categorical import CategoricalHyperparameter from ConfigSpace.hyperparameters.constant import Constant, UnParametrizedHyperparameter from ConfigSpace.hyperparameters.float_hyperparameter import FloatHyperparameter -from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter, NotSet +from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter from ConfigSpace.hyperparameters.integer_hyperparameter import IntegerHyperparameter from ConfigSpace.hyperparameters.normal_float import NormalFloatHyperparameter from ConfigSpace.hyperparameters.normal_integer import NormalIntegerHyperparameter @@ -27,5 +27,4 @@ "NormalIntegerHyperparameter", "BetaFloatHyperparameter", "BetaIntegerHyperparameter", - "NotSet", ] diff --git a/ConfigSpace/hyperparameters/hyperparameter.py b/ConfigSpace/hyperparameters/hyperparameter.py index 7a998095..ed884c7f 100644 --- a/ConfigSpace/hyperparameters/hyperparameter.py +++ b/ConfigSpace/hyperparameters/hyperparameter.py @@ -4,7 +4,6 @@ import numpy as np -NotSet = object() class Comparison(Enum): """Enumeration of possible comparison results.""" diff --git a/ConfigSpace/hyperparameters/normal_float.py b/ConfigSpace/hyperparameters/normal_float.py index 31055f28..6d958e78 100644 --- a/ConfigSpace/hyperparameters/normal_float.py +++ b/ConfigSpace/hyperparameters/normal_float.py @@ -125,7 +125,10 @@ def __repr__(self) -> str: if self.lower is None or self.upper is None: repr_str.write( - f"{self.name}, Type: NormalFloat, Mu: {self.mu!r} Sigma: {self.sigma!r}, Default: {self.default_value!r}", + f"{self.name}, Type: NormalFloat," + f" Mu: {self.mu!r}" + f" Sigma: {self.sigma!r}," + f" Default: {self.default_value!r}", ) else: repr_str.write( diff --git a/ConfigSpace/hyperparameters/ordinal.py b/ConfigSpace/hyperparameters/ordinal.py index 1bd0ad69..8f7d4075 100644 --- a/ConfigSpace/hyperparameters/ordinal.py +++ b/ConfigSpace/hyperparameters/ordinal.py @@ -2,7 +2,6 @@ import copy import io -from collections import OrderedDict from typing import Any import numpy as np diff --git a/ConfigSpace/hyperparameters/uniform_integer.py b/ConfigSpace/hyperparameters/uniform_integer.py index be06142e..ac841a12 100644 --- a/ConfigSpace/hyperparameters/uniform_integer.py +++ b/ConfigSpace/hyperparameters/uniform_integer.py @@ -59,7 +59,7 @@ def __init__( if q < 1: raise ValueError( "Setting quantization < 1 for Integer " - f"Hyperparameter {name} has no effect." + f"Hyperparameter {name} has no effect.", ) self.q = self.check_int(q, "q") @@ -102,12 +102,14 @@ def __init__( def __repr__(self) -> str: repr_str = io.StringIO() repr_str.write( - f"{self.name}, Type: UniformInteger, Range: [{self.lower!r}, {self.upper!r}], Default: {self.default_value!r}", + f"{self.name}, Type: UniformInteger," + f" Range: [{self.lower!r}, {self.upper!r}]," + f" Default: {self.default_value!r}", ) if self.log: repr_str.write(", on log-scale") if self.q is not None: - repr_str.write(", Q: %s" % repr(self.q)) + repr_str.write(f", Q: {self.q}") repr_str.seek(0) return repr_str.getvalue() @@ -165,8 +167,8 @@ def check_default(self, default_value: int | float | None) -> int: if self.is_legal(default_value): return default_value - else: - raise ValueError("Illegal default value %s" % str(default_value)) + + raise ValueError("Illegal default value %s" % str(default_value)) def has_neighbors(self) -> bool: if self.log: @@ -179,13 +181,13 @@ def has_neighbors(self) -> bool: # If there is only one active value, this is not enough return upper - lower >= 1 - def get_num_neighbors(self, value=None) -> int: + def get_num_neighbors(self, value: int | None = None) -> int: # If there is a value in the range, then that value is not a neighbor of itself # so we need to remove one if value is not None and self.lower <= value <= self.upper: return self.upper - self.lower - 1 - else: - return self.upper - self.lower + + return self.upper - self.lower def get_neighbors( self, @@ -252,8 +254,8 @@ def get_neighbors( if transform: return neighbors - else: - return self._inverse_transform(np.asarray(neighbors)).tolist() + + return self._inverse_transform(np.asarray(neighbors)).tolist() # A truncated normal between 0 and 1, centered on the value with a scale of std. # This will be sampled from and converted to the corresponding int value @@ -297,8 +299,8 @@ def get_neighbors( neighbors = list(seen) if transform: return neighbors - else: - return self._inverse_transform(np.array(neighbors)).tolist() + + return self._inverse_transform(np.array(neighbors)).tolist() def _pdf(self, vector: np.ndarray) -> np.ndarray: """ diff --git a/ConfigSpace/util.py b/ConfigSpace/util.py index 208ff1bb..7455420a 100644 --- a/ConfigSpace/util.py +++ b/ConfigSpace/util.py @@ -172,8 +172,8 @@ def get_one_exchange_neighbourhood( array = configuration.get_array() # type: np.ndarray value = array[index] # type: float - # Check for NaNs (inactive value) - if value != value: + # Inactive value + if isinstance(value, float) and np.isnan(value): continue iteration = 0 diff --git a/pyproject.toml b/pyproject.toml index c5dcfd1c..dd1b26fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -178,6 +178,7 @@ select = [ ignore = [ "D100", "D101", # Missing docstring in public class + "D102", # Missing docstring in public method (Should be active but it's too much for right now) "D104", # Missing docstring in public package "D105", # Missing docstring in magic mthod "D203", # 1 blank line required before class docstring diff --git a/test/test_configspace_from_dict.py b/test/test_configspace_from_dict.py index dcf517b8..fcda60ce 100644 --- a/test/test_configspace_from_dict.py +++ b/test/test_configspace_from_dict.py @@ -81,12 +81,6 @@ ], ) def test_individual_hyperparameters(value: Any, expected: Hyperparameter) -> None: - """ - Expects - ------- - * Creating a constant with the dictionary easy api will insert a Constant - into it's hyperparameters. - """ cs = ConfigurationSpace({"hp": value}) assert cs["hp"] == expected From 79decbd85811f7cf70fb6cc2bb76936f3c4a5c17 Mon Sep 17 00:00:00 2001 From: eddiebergman Date: Tue, 19 Dec 2023 14:58:49 +0100 Subject: [PATCH 004/104] refactor: UniformFloat --- ConfigSpace/deprecate.py | 27 ++ ConfigSpace/hyperparameters/uniform_float.py | 252 +++++++++++-------- 2 files changed, 172 insertions(+), 107 deletions(-) create mode 100644 ConfigSpace/deprecate.py diff --git a/ConfigSpace/deprecate.py b/ConfigSpace/deprecate.py new file mode 100644 index 00000000..db5bf338 --- /dev/null +++ b/ConfigSpace/deprecate.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import warnings +from typing import Any + + +def deprecate( + thing: Any, + instead: str, + stacklevel: int = 3, +) -> None: + """Deprecate a thing and warn when it is used. + + Parameters + ---------- + thing : Any + The thing to deprecate. + + instead : str + What to use instead. + + stacklevel : int, optional + How many levels up in the stack to place the warning. + Defaults to 3. + """ + msg = f"{thing} is deprecated and will be removed in a future version." f"\n{instead}" + warnings.warn(msg, stacklevel=stacklevel) diff --git a/ConfigSpace/hyperparameters/uniform_float.py b/ConfigSpace/hyperparameters/uniform_float.py index 5eda0f1e..69618276 100644 --- a/ConfigSpace/hyperparameters/uniform_float.py +++ b/ConfigSpace/hyperparameters/uniform_float.py @@ -1,11 +1,12 @@ from __future__ import annotations import io -import math -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, overload import numpy as np +import numpy.typing as npt +from ConfigSpace.deprecate import deprecate from ConfigSpace.hyperparameters.float_hyperparameter import FloatHyperparameter if TYPE_CHECKING: @@ -53,90 +54,74 @@ def __init__( Field for holding meta data provided by the user. Not used by the configuration space. """ - default_value = None if default_value is None else float(default_value) - super().__init__(name, default_value, meta) - self.lower = float(lower) - self.upper = float(upper) - self.q = float(q) if q is not None else None - self.log = bool(log) - - if self.lower >= self.upper: + lower = float(lower) + upper = float(upper) + q = float(q) if q is not None else None + log = bool(log) + if lower >= upper: raise ValueError( - f"Upper bound {self.upper:f} must be larger than lower bound " - f"{self.lower:f} for hyperparameter {name}", + f"Upper bound {upper:f} must be larger than lower bound " + f"{lower:f} for hyperparameter {name}", ) - elif log and self.lower <= 0: + + if log and lower <= 0: raise ValueError( - f"Negative lower bound ({self.lower:f}) for log-scale " + f"Negative lower bound ({lower:f}) for log-scale " f"hyperparameter {name} is forbidden.", ) - self.default_value = self.check_default(default_value) + if q is not None and (np.round((upper - lower) % q, 10) not in (0, q)): + # There can be weird rounding errors, so we compare the result against self.q + # for example, 2.4 % 0.2 = 0.1999999999999998 + diff = upper - lower + raise ValueError( + f"Upper bound minus lower bound ({upper:f} - {lower:f} = {diff}) must be" + f" a multiple of q ({q})", + ) - if self.log: - if self.q is not None: - lower = self.lower - (np.float64(self.q) / 2.0 - 0.0001) - upper = self.upper + (np.float64(self.q) / 2.0 - 0.0001) - else: - lower = self.lower - upper = self.upper - self._lower = np.log(lower) - self._upper = np.log(upper) + self.lower = lower + self.upper = upper + self.q = q + self.log = log + + q_lower, q_upper = (lower, upper) + if q is not None: + q_lower = lower - (q / 2.0 - 0.0001) + q_upper = upper + (q / 2.0 - 0.0001) + + if log: + self._lower = np.log(q_lower) + self._upper = np.log(q_upper) else: - if self.q is not None: - self._lower = self.lower - (self.q / 2.0 - 0.0001) - self._upper = self.upper + (self.q / 2.0 - 0.0001) + self._lower = q_lower + self._upper = q_upper + + if default_value is None: + if log: + default_value = float(np.exp((np.log(lower) + np.log(upper)) / 2.0)) else: - self._lower = self.lower - self._upper = self.upper - if self.q is not None: - # There can be weird rounding errors, so we compare the result against self.q, see - # In [13]: 2.4 % 0.2 - # Out[13]: 0.1999999999999998 - if np.round((self.upper - self.lower) % self.q, 10) not in (0, self.q): - raise ValueError( - f"Upper bound ({self.upper:f}) - lower bound ({self.lower:f}) must be a multiple of q ({self.q:f})", - ) + default_value = (lower + upper) / 2.0 - self.normalized_default_value = self._inverse_transform(self.default_value) + default_value = float(np.round(default_value, 10)) + if not self.is_legal(default_value): + raise ValueError(f"Illegal default value {default_value}") - def __repr__(self) -> str: - repr_str = io.StringIO() - repr_str.write( - f"{self.name}, Type: UniformFloat, Range: [{self.lower!r}, {self.upper!r}], Default: {self.default_value!r}", - ) - if self.log: - repr_str.write(", on log-scale") - if self.q is not None: - repr_str.write(", Q: %s" % str(self.q)) - repr_str.seek(0) - return repr_str.getvalue() + self.default_value = default_value + self.normalized_default_value = self._inverse_transform(self.default_value) + super().__init__(name=name, default_value=default_value, meta=meta) def is_legal(self, value: float) -> bool: if not isinstance(value, (float, int)): return False return self.upper >= value >= self.lower - def is_legal_vector(self, value) -> bool: + def is_legal_vector(self, value: float) -> bool: + # NOTE: This really needs a better name as it doesn't operate on vectors, + # it means that it operates on the normalzied space, i.e. what is gotten + # by inverse_transform return 1.0 >= value >= 0.0 - def check_default(self, default_value: float | int | None) -> float: - if default_value is None: - if self.log: - default_value = float(np.exp((np.log(self.lower) + np.log(self.upper)) / 2.0)) - else: - default_value = (self.lower + self.upper) / 2.0 - default_value = float(np.round(default_value, 10)) - - if self.is_legal(default_value): - return default_value - else: - raise ValueError("Illegal default value %s" % str(default_value)) - def to_integer(self) -> UniformIntegerHyperparameter: - # TODO check if conversion makes sense at all (at least two integer values possible!) - # todo check if params should be converted to int while class initialization - # or inside class itself from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter return UniformIntegerHyperparameter( @@ -146,67 +131,106 @@ def to_integer(self) -> UniformIntegerHyperparameter: default_value=int(np.rint(self.default_value)), q=int(np.rint(self.q)) if self.q is not None else None, log=self.log, + meta=None, ) - def _sample(self, rs: np.random, size: int | None = None) -> float | np.ndarray: + @overload + def _sample(self, rs: np.random.RandomState, size: None = None) -> float: + ... + + @overload + def _sample(self, rs: np.random.RandomState, size: int) -> npt.NDArray[np.float64]: + ... + + def _sample( + self, + rs: np.random.RandomState, + size: int | None = None, + ) -> float | npt.NDArray[np.float64]: return rs.uniform(size=size) - def _transform_vector(self, vector: np.ndarray) -> np.ndarray: - if np.isnan(vector).any(): - raise ValueError("Vector %s contains NaN's" % vector) + def _transform_scalar(self, scalar: float) -> np.float64: + deprecate(self._transform_scalar, "Please use _transform instead") + return self._transform(scalar) + + def _transform_vector(self, vector: npt.NDArray[np.number]) -> npt.NDArray[np.float64]: + deprecate(self._transform_scalar, "Please use _transform instead") + return self._transform(vector) + + @overload + def _transform(self, vector: float) -> np.float64: + ... + + @overload + def _transform(self, vector: npt.NDArray[np.number]) -> npt.NDArray[np.float64]: + ... + + def _transform( + self, + vector: npt.NDArray[np.number] | float, + ) -> npt.NDArray[np.float64] | np.float64: vector = vector * (self._upper - self._lower) + self._lower if self.log: - vector = np.exp(vector) - if self.q is not None: - vector = np.rint((vector - self.lower) / self.q) * self.q + self.lower - vector = np.minimum(vector, self.upper) - vector = np.maximum(vector, self.lower) - return np.maximum(self.lower, np.minimum(self.upper, vector)) - - def _transform_scalar(self, scalar: float) -> float: - if scalar != scalar: - raise ValueError("Number %s is NaN" % scalar) - scalar = scalar * (self._upper - self._lower) + self._lower - if self.log: - scalar = math.exp(scalar) + vector = np.exp(vector, dtype=np.float64) + if self.q is not None: - scalar = np.round((scalar - self.lower) / self.q) * self.q + self.lower - scalar = min(scalar, self.upper) - scalar = max(scalar, self.lower) - return min(self.upper, max(self.lower, scalar)) + quantized = (vector - self.lower) / self.q + vector = np.rint(quantized) * self.q + self.lower + + return np.clip(vector, self.lower, self.upper, dtype=np.float64) + + @overload + def _inverse_transform(self, vector: float) -> np.float64: + ... + + @overload + def _inverse_transform(self, vector: npt.NDArray[np.number]) -> npt.NDArray[np.float64]: + ... def _inverse_transform( self, - vector: np.ndarray | float | int | None, - ) -> np.ndarray | float: - if vector is None: - return np.NaN + vector: npt.NDArray[np.number] | float, + ) -> npt.NDArray[np.float64] | np.float64: + """Converts a value from the original space to the transformed space (0, 1).""" if self.log: vector = np.log(vector) vector = (vector - self._lower) / (self._upper - self._lower) - vector = np.minimum(1.0, vector) - return np.maximum(0.0, vector) + return np.clip(vector, 0.0, 1.0, dtype=np.float64) def get_neighbors( self, - value: Any, + value: float, # Should be normalized closely into 0, 1! rs: np.random.RandomState, number: int = 4, transform: bool = False, std: float = 0.2, - ) -> list[float]: - neighbors = [] # type: List[float] - while len(neighbors) < number: - neighbor = rs.normal(value, std) # type: float - if neighbor < 0 or neighbor > 1: - continue - if transform: - neighbors.append(self._transform(neighbor)) - else: - neighbors.append(neighbor) + ) -> npt.NDArray[np.float64]: + BUFFER_MULTIPLIER = 2 + SAMPLE_SIZE = number * BUFFER_MULTIPLIER + # Make sure we can accomidate the number of (neighbors - 1) + a new sample set + BUFFER_SIZE = number + number * BUFFER_MULTIPLIER + + neighbors = np.empty(BUFFER_SIZE, dtype=np.float64) + offset = 0 + + # Generate batches of number * 2 candidates, filling the above + # buffer until we have enough valid candidates. + # We should not overflow as the buffer + while offset <= number: + candidates = rs.normal(value, std, size=SAMPLE_SIZE) + valid_candidates = candidates[(candidates >= 0) & (candidates <= 1)] + + n_candidates = len(valid_candidates) + neighbors[offset:n_candidates] = valid_candidates + offset += n_candidates + + neighbors = neighbors[:number] + if transform: + return self._transform(neighbors) + return neighbors - def _pdf(self, vector: np.ndarray) -> np.ndarray: + def _pdf(self, vector: npt.NDArray[np.number]) -> npt.NDArray[np.float64]: """ Computes the probability density function of the parameter in the transformed (and possibly normalized, depends on the parameter @@ -230,8 +254,8 @@ def _pdf(self, vector: np.ndarray) -> np.ndarray: # or lower bound. ub = 1 lb = 0 - inside_range = ((lb <= vector) & (vector <= ub)).astype(int) - return inside_range / (self.upper - self.lower) + inside_range = ((lb <= vector) & (vector <= ub)).astype(dtype=np.uint64) + return np.true_divide(inside_range, self.upper - self.lower, dtype=np.float64) def get_max_density(self) -> float: return 1 / (self.upper - self.lower) @@ -241,3 +265,17 @@ def get_size(self) -> float: return np.inf return np.rint((self.upper - self.lower) / self.q) + 1 + + def __repr__(self) -> str: + repr_str = io.StringIO() + repr_str.write( + f"{self.name}, Type: UniformFloat, " + f"Range: [{self.lower!r}, {self.upper!r}], " + f"Default: {self.default_value!r}", + ) + if self.log: + repr_str.write(", on log-scale") + if self.q is not None: + repr_str.write(", Q: %s" % str(self.q)) + repr_str.seek(0) + return repr_str.getvalue() From add84b5294dccb4e1da93d630925e69560f2982f Mon Sep 17 00:00:00 2001 From: eddiebergman Date: Tue, 19 Dec 2023 15:11:00 +0100 Subject: [PATCH 005/104] optim: UniformFloat get neighbors --- ConfigSpace/hyperparameters/uniform_float.py | 13 +++++++++---- test/test_hyperparameters.py | 18 ++++++++++-------- test/test_util.py | 11 ++++++----- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/ConfigSpace/hyperparameters/uniform_float.py b/ConfigSpace/hyperparameters/uniform_float.py index 69618276..1130a756 100644 --- a/ConfigSpace/hyperparameters/uniform_float.py +++ b/ConfigSpace/hyperparameters/uniform_float.py @@ -110,13 +110,18 @@ def __init__( self.normalized_default_value = self._inverse_transform(self.default_value) super().__init__(name=name, default_value=default_value, meta=meta) - def is_legal(self, value: float) -> bool: + def is_legal(self, value: float, *, normalized: bool = False) -> bool: if not isinstance(value, (float, int)): return False - return self.upper >= value >= self.lower + + if normalized: + return 0.0 <= value <= 1.0 + + return self.lower <= value <= self.upper def is_legal_vector(self, value: float) -> bool: # NOTE: This really needs a better name as it doesn't operate on vectors, + # rather individual values. # it means that it operates on the normalzied space, i.e. what is gotten # by inverse_transform return 1.0 >= value >= 0.0 @@ -216,8 +221,8 @@ def get_neighbors( # Generate batches of number * 2 candidates, filling the above # buffer until we have enough valid candidates. # We should not overflow as the buffer - while offset <= number: - candidates = rs.normal(value, std, size=SAMPLE_SIZE) + while offset < number: + candidates = rs.normal(value, std, size=(SAMPLE_SIZE,)) valid_candidates = candidates[(candidates >= 0) & (candidates <= 1)] n_candidates = len(valid_candidates) diff --git a/test/test_hyperparameters.py b/test/test_hyperparameters.py index f5579cb0..bc025203 100644 --- a/test/test_hyperparameters.py +++ b/test/test_hyperparameters.py @@ -277,7 +277,6 @@ def test_uniformfloat_is_legal(): assert not f1.is_legal_vector(-0.1) assert not f1.is_legal_vector(1.1) - with pytest.raises(TypeError): assert not f1.is_legal_vector("Hahaha") @@ -742,9 +741,9 @@ def test_betafloat(): b1 = BetaFloatHyperparameter("param", lower=0.0, upper=1.0, alpha=3.0, beta=1.0) # with identical domains, beta and uniform should sample the same points - assert u1.get_neighbors(0.5, rs=np.random.RandomState(42)) == b1.get_neighbors( - 0.5, - rs=np.random.RandomState(42), + np.testing.assert_equal( + u1.get_neighbors(0.5, rs=np.random.RandomState(42)), + b1.get_neighbors(0.5, rs=np.random.RandomState(42)), ) # Test copy copy_f1 = copy.copy(f1) @@ -1186,9 +1185,7 @@ def test_uniforminteger(): assert f1.log is False assert f1.normalized_default_value == pytest.approx((2.0 + 0.49999) / (5.49999 + 0.49999)) - quantization_warning = ( - "Setting quantization < 1 for Integer Hyperparameter param has no effect" - ) + quantization_warning = "Setting quantization < 1 for Integer Hyperparameter param has no effect" with pytest.raises(ValueError, match=quantization_warning): _ = UniformIntegerHyperparameter("param", 0, 10, q=0.1) # type: ignore with pytest.raises(ValueError, match=quantization_warning): @@ -1881,7 +1878,12 @@ def test_betaint_legal_float_values(): match="Illegal default value 0.5", ): _ = BetaIntegerHyperparameter( - "param", lower=-2.0, upper=2.0, alpha=3.0, beta=1.1, default_value=0.5, + "param", + lower=-2.0, + upper=2.0, + alpha=3.0, + beta=1.1, + default_value=0.5, ) diff --git a/test/test_util.py b/test/test_util.py index 8d7379aa..0e2e83f9 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -79,6 +79,7 @@ def _test_get_one_exchange_neighbourhood(hp): num_neighbors = 0 if not isinstance(hp, list): hp = [hp] + for hp_ in hp: cs.add_hyperparameter(hp_) if np.isinf(hp_.get_num_neighbors()): @@ -90,7 +91,7 @@ def _test_get_one_exchange_neighbourhood(hp): config = cs.get_default_configuration() all_neighbors = [] for i in range(100): - neighborhood = get_one_exchange_neighbourhood(config, i) + neighborhood = get_one_exchange_neighbourhood(config, i, num_neighbors=num_neighbors) for new_config in neighborhood: assert config != new_config assert dict(config) != dict(new_config) @@ -129,14 +130,14 @@ def test_random_neighborhood_float(): hp = UniformFloatHyperparameter("a", 1, 10) all_neighbors = _test_get_one_exchange_neighbourhood(hp) all_neighbors = [neighbor["a"] for neighbor in all_neighbors] - assert pytest.approx(np.mean(all_neighbors), abs=1e-2) == 5.49 - assert pytest.approx(np.var(all_neighbors), abs=1e-2) == 3.192 + assert pytest.approx(np.mean(all_neighbors), abs=1e-2) == 5.47 + assert pytest.approx(np.var(all_neighbors), abs=1e-2) == 2.78 hp = UniformFloatHyperparameter("a", 1, 10, log=True) all_neighbors = _test_get_one_exchange_neighbourhood(hp) all_neighbors = [neighbor["a"] for neighbor in all_neighbors] # Default value is 3.16 - assert pytest.approx(np.mean(all_neighbors), abs=1e-2) == 3.50 - assert pytest.approx(np.var(all_neighbors), abs=1e-2) == 2.79 + assert pytest.approx(np.mean(all_neighbors), abs=1e-2) == 3.43 + assert pytest.approx(np.var(all_neighbors), abs=1e-2) == 2.17 def test_random_neighbor_int(): From a3f42978dafc03f42c4433731f767ca94fefcf72 Mon Sep 17 00:00:00 2001 From: eddiebergman Date: Tue, 19 Dec 2023 15:11:58 +0100 Subject: [PATCH 006/104] refactor: deprecate UniformFloat is_legal_vector --- ConfigSpace/hyperparameters/uniform_float.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ConfigSpace/hyperparameters/uniform_float.py b/ConfigSpace/hyperparameters/uniform_float.py index 1130a756..6a1567a6 100644 --- a/ConfigSpace/hyperparameters/uniform_float.py +++ b/ConfigSpace/hyperparameters/uniform_float.py @@ -124,6 +124,7 @@ def is_legal_vector(self, value: float) -> bool: # rather individual values. # it means that it operates on the normalzied space, i.e. what is gotten # by inverse_transform + deprecate(self.is_legal_vector, "Please use is_legal(..., normalized=True) instead") return 1.0 >= value >= 0.0 def to_integer(self) -> UniformIntegerHyperparameter: From 60dc4dba590d48f3ed1b9bf9ac0b23b4ee70fa95 Mon Sep 17 00:00:00 2001 From: eddiebergman Date: Wed, 6 Mar 2024 17:11:06 +0100 Subject: [PATCH 007/104] big overhaul --- ConfigSpace/api/types/categorical.py | 10 +- ConfigSpace/api/types/float.py | 6 +- ConfigSpace/api/types/integer.py | 6 +- ConfigSpace/c_util.py | 25 +- ConfigSpace/conditions.pxd | 15 - ConfigSpace/conditions.py | 99 ++-- ConfigSpace/configuration.py | 83 ++-- ConfigSpace/configuration_space.py | 283 ++++++----- ConfigSpace/deprecate.py | 27 - ConfigSpace/forbidden.pxd | 24 - ConfigSpace/forbidden.py | 117 +++-- ConfigSpace/functional.py | 132 ++++- ConfigSpace/hyperparameters/__init__.py | 26 +- ConfigSpace/hyperparameters/_distributions.py | 410 +++++++++++++++ ConfigSpace/hyperparameters/_hp_components.py | 185 +++++++ ConfigSpace/hyperparameters/beta_float.pxd | 19 - ConfigSpace/hyperparameters/beta_float.py | 304 ++++-------- ConfigSpace/hyperparameters/beta_integer.pxd | 21 - ConfigSpace/hyperparameters/beta_integer.py | 285 ++++------- ConfigSpace/hyperparameters/categorical.py | 469 +++++------------- ConfigSpace/hyperparameters/constant.py | 189 ++----- .../hyperparameters/float_hyperparameter.pxd | 19 - .../hyperparameters/float_hyperparameter.py | 98 +--- .../hyperparameters/hyperparameter.pxd | 22 - ConfigSpace/hyperparameters/hyperparameter.py | 457 +++++++++++------ .../integer_hyperparameter.pxd | 20 - .../hyperparameters/integer_hyperparameter.py | 108 +--- ConfigSpace/hyperparameters/normal_float.pxd | 19 - ConfigSpace/hyperparameters/normal_float.py | 409 +++------------ .../hyperparameters/normal_integer.pxd | 21 - ConfigSpace/hyperparameters/normal_integer.py | 447 +++-------------- ConfigSpace/hyperparameters/numerical.pxd | 25 - ConfigSpace/hyperparameters/numerical.py | 78 --- .../numerical_hyperparameter.py | 15 + ConfigSpace/hyperparameters/ordinal.py | 367 ++------------ ConfigSpace/hyperparameters/uniform_float.pxd | 18 - ConfigSpace/hyperparameters/uniform_float.py | 308 ++---------- .../hyperparameters/uniform_integer.pxd | 18 - .../hyperparameters/uniform_integer.py | 368 ++------------ ConfigSpace/read_and_write/json.py | 39 +- ConfigSpace/read_and_write/pcs.py | 76 ++- ConfigSpace/read_and_write/pcs_new.py | 94 ++-- ConfigSpace/util.py | 133 ++--- pyproject.toml | 65 +-- setup.py | 121 ----- 45 files changed, 2265 insertions(+), 3815 deletions(-) delete mode 100644 ConfigSpace/conditions.pxd delete mode 100644 ConfigSpace/deprecate.py delete mode 100644 ConfigSpace/forbidden.pxd create mode 100644 ConfigSpace/hyperparameters/_distributions.py create mode 100644 ConfigSpace/hyperparameters/_hp_components.py delete mode 100644 ConfigSpace/hyperparameters/beta_float.pxd delete mode 100644 ConfigSpace/hyperparameters/beta_integer.pxd delete mode 100644 ConfigSpace/hyperparameters/float_hyperparameter.pxd delete mode 100644 ConfigSpace/hyperparameters/hyperparameter.pxd delete mode 100644 ConfigSpace/hyperparameters/integer_hyperparameter.pxd delete mode 100644 ConfigSpace/hyperparameters/normal_float.pxd delete mode 100644 ConfigSpace/hyperparameters/normal_integer.pxd delete mode 100644 ConfigSpace/hyperparameters/numerical.pxd delete mode 100644 ConfigSpace/hyperparameters/numerical.py create mode 100644 ConfigSpace/hyperparameters/numerical_hyperparameter.py delete mode 100644 ConfigSpace/hyperparameters/uniform_float.pxd delete mode 100644 ConfigSpace/hyperparameters/uniform_integer.pxd diff --git a/ConfigSpace/api/types/categorical.py b/ConfigSpace/api/types/categorical.py index c0c84a27..e4739529 100644 --- a/ConfigSpace/api/types/categorical.py +++ b/ConfigSpace/api/types/categorical.py @@ -1,16 +1,16 @@ from __future__ import annotations +from collections.abc import Sequence from typing import ( - Sequence, - Union, + Literal, + TypeAlias, overload, ) -from typing_extensions import Literal, TypeAlias from ConfigSpace.hyperparameters import CategoricalHyperparameter, OrdinalHyperparameter # We only accept these types in `items` -T: TypeAlias = Union[str, int, float] +T: TypeAlias = str | int | float # ordered False -> CategoricalHyperparameter @@ -88,7 +88,7 @@ def Categorical( # Add some meta information for your own tracking c = Categorical("animals", ["cat", "dog", "mouse"], meta={"use": "Favourite Animal"}) - Note + Note: ---- ``Categorical`` is actually a function, please use the corresponding return types if doing an `isinstance(param, type)` check with either diff --git a/ConfigSpace/api/types/float.py b/ConfigSpace/api/types/float.py index bf3bc559..4daa0cdc 100644 --- a/ConfigSpace/api/types/float.py +++ b/ConfigSpace/api/types/float.py @@ -93,7 +93,7 @@ def Float( # Add meta info to the param Float("a", (1.0, 10), meta={"use": "For counting chickens"}) - Note + Note: ---- `Float` is actually a function, please use the corresponding return types if doing an `isinstance(param, type)` check and not `Float`. @@ -116,7 +116,7 @@ def Float( q : float | None = None The quantization factor, must evenly divide the boundaries. - Note + Note: ---- Quantization points act are not equal and require experimentation to be certain about @@ -129,7 +129,7 @@ def Float( meta : dict | None = None Any meta information you want to associate with this parameter - Returns + Returns: ------- UniformFloatHyperparameter | NormalFloatHyperparameter | BetaFloatHyperparameter Returns the corresponding hyperparameter type diff --git a/ConfigSpace/api/types/integer.py b/ConfigSpace/api/types/integer.py index 099cbdbc..885b9063 100644 --- a/ConfigSpace/api/types/integer.py +++ b/ConfigSpace/api/types/integer.py @@ -93,7 +93,7 @@ def Integer( # Add meta info to the param Integer("a", (1, 10), meta={"use": "For counting chickens"}) - Note + Note: ---- `Integer` is actually a function, please use the corresponding return types if doing an `isinstance(param, type)` check and not `Integer`. @@ -126,7 +126,7 @@ def Integer( All samples here will then be in {1, 4, 7, 10} - Note + Note: ---- Quantization points act are not equal and require experimentation to be certain about @@ -139,7 +139,7 @@ def Integer( meta : dict | None = None Any meta information you want to associate with this parameter - Returns + Returns: ------- UniformIntegerHyperparameter | NormalIntegerHyperparameter | BetaIntegerHyperparameter Returns the corresponding hyperparameter type diff --git a/ConfigSpace/c_util.py b/ConfigSpace/c_util.py index 61ea61f1..fa245268 100644 --- a/ConfigSpace/c_util.py +++ b/ConfigSpace/c_util.py @@ -26,7 +26,9 @@ def check_forbidden(forbidden_clauses: list, vector: np.ndarray) -> int: for i in range(Iforbidden): clause = forbidden_clauses[i] if clause.c_is_forbidden_vector(vector, strict=False): - raise ForbiddenValueError("Given vector violates forbidden clause %s" % (str(clause))) + raise ForbiddenValueError( + "Given vector violates forbidden clause %s" % (str(clause)), + ) def check_configuration( @@ -64,7 +66,7 @@ def check_configuration( hyperparameter = self._hyperparameters[hp_name] hp_value = vector[hp_idx] - if not np.isnan(hp_value) and not hyperparameter.is_legal_vector(hp_value): + if not np.isnan(hp_value) and not hyperparameter.legal_vector(hp_value): raise IllegalValueError(hyperparameter, hp_value) children = self._children_of[hp_name] @@ -79,14 +81,18 @@ def check_configuration( break if add: hyperparameter_idx = self._hyperparameter_idx[child.name] - active[hyperparameter_idx] = True + active[hyperparameter_idx] = True to_visit.appendleft(child.name) if active[hp_idx] is True and np.isnan(hp_value): raise ActiveHyperparameterNotSetError(hyperparameter) for hp_idx in self._idx_to_hyperparameter: - if not allow_inactive_with_values and not active[hp_idx] and not np.isnan(vector[hp_idx]): + if ( + not allow_inactive_with_values + and not active[hp_idx] + and not np.isnan(vector[hp_idx]) + ): # Only look up the value (in the line above) if the hyperparameter is inactive! hp_name = self._idx_to_hyperparameter[hp_idx] hp_value = vector[hp_idx] @@ -110,7 +116,7 @@ def correct_sampled_array( clause: AbstractForbiddenComponent condition: ConditionComponent hyperparameter_idx: int - NaN: float = np.NaN + NaN: float = np.nan visited: set inactive: set child: Hyperparameter @@ -171,9 +177,8 @@ def correct_sampled_array( conditions = parent_conditions_of[child_name] if isinstance(conditions[0], OrConjunction): pass - else: # AndCondition - if parents_visited != len(parents): - continue + elif parents_visited != len(parents): + continue add = True for j in range(len(conditions)): @@ -229,7 +234,7 @@ def change_hp_value( index : int - Returns + Returns: ------- np.ndarray """ @@ -248,7 +253,7 @@ def change_hp_value( ch: Hyperparameter child: str to_disable: set - NaN: float = np.NaN + NaN: float = np.nan children_of: dict = configuration_space._children_of configuration_array[index] = hp_value diff --git a/ConfigSpace/conditions.pxd b/ConfigSpace/conditions.pxd deleted file mode 100644 index 8c7af891..00000000 --- a/ConfigSpace/conditions.pxd +++ /dev/null @@ -1,15 +0,0 @@ -import numpy as np -cimport numpy as np - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t - - -cdef class ConditionComponent(object): - cdef int _evaluate_vector(self, np.ndarray value) diff --git a/ConfigSpace/conditions.py b/ConfigSpace/conditions.py index caba3f2d..5952b4d6 100644 --- a/ConfigSpace/conditions.py +++ b/ConfigSpace/conditions.py @@ -89,7 +89,8 @@ class AbstractCondition(ConditionComponent): def __init__(self, child: Hyperparameter, parent: Hyperparameter) -> None: if child == parent: raise ValueError( - "The child and parent hyperparameter must be different " "hyperparameters.", + "The child and parent hyperparameter must be different " + "hyperparameters.", ) self.child = child self.parent = parent @@ -97,8 +98,7 @@ def __init__(self, child: Hyperparameter, parent: Hyperparameter) -> None: self.parent_vector_id = -1 def __eq__(self, other: Any) -> bool: - """ - Defines the __ne__() as stated in the + """Defines the __ne__() as stated in the documentation from python: By default, object implements __eq__() by using is, returning NotImplemented in the case of a false comparison: True if x is y else NotImplemented. @@ -138,13 +138,15 @@ def evaluate( instantiated_parent_hyperparameter: dict[str, int | float | str], ) -> bool: value = instantiated_parent_hyperparameter[self.parent.name] - if isinstance(value, (float, int, np.number)) and np.isnan(value): + if isinstance(value, float | int | np.number) and np.isnan(value): return False return self._evaluate(value) def _evaluate_vector(self, instantiated_vector: np.ndarray) -> bool: if self.parent_vector_id is None: - raise ValueError("Parent vector id should not be None when calling evaluate vector") + raise ValueError( + "Parent vector id should not be None when calling evaluate vector", + ) return self._inner_evaluate_vector(instantiated_vector[self.parent_vector_id]) @@ -190,14 +192,14 @@ def __init__( Value, which the parent is compared to """ super().__init__(child, parent) - if not parent.is_legal(value): + if not parent.legal_value(value): raise ValueError( f"Hyperparameter '{child.name}' is " f"conditional on the illegal value '{value}' of " f"its parent hyperparameter '{parent.name}'", ) self.value = value - self.vector_value = self.parent._inverse_transform(self.value) + self.vector_value = self.parent.to_vector(self.value) def __repr__(self) -> str: return f"{self.child.name} | {self.parent.name} == {self.value!r}" @@ -212,7 +214,7 @@ def __copy__(self): def _evaluate(self, value: str | float | int) -> bool: # No need to check if the value to compare is a legal value; either it # is equal (and thus legal), or it would evaluate to False anyway - return self.parent.compare(value, self.value) is Comparison.EQUAL + return self.parent.compare_value(value, self.value) is Comparison.EQUAL def _inner_evaluate_vector(self, value) -> bool: # No need to check if the value to compare is a legal value; either it @@ -254,14 +256,14 @@ def __init__( Value, which the parent is compared to """ super().__init__(child, parent) - if not parent.is_legal(value): + if not parent.legal_value(value): raise ValueError( f"Hyperparameter '{child.name}' is " f"conditional on the illegal value '{value}' of " f"its parent hyperparameter '{parent.name}'", ) self.value = value - self.vector_value = self.parent._inverse_transform(self.value) + self.vector_value = self.parent.to_vector(self.value) def __repr__(self) -> str: return f"{self.child.name} | {self.parent.name} != {self.value!r}" @@ -274,15 +276,17 @@ def __copy__(self): ) def _evaluate(self, value: str | float | int) -> bool: - if not self.parent.is_legal(value): + if not self.parent.legal_value(value): return False - return self.parent.compare(value, self.value) is not Comparison.EQUAL + return self.parent.compare_value(value, self.value) is not Comparison.EQUAL def _inner_evaluate_vector(self, value) -> bool: - if not self.parent.is_legal_vector(value): + if not self.parent.legal_vector(value): return False - return self.parent.compare_vector(value, self.vector_value) is not Comparison.EQUAL + return ( + self.parent.compare_vector(value, self.vector_value) is not Comparison.EQUAL + ) class LessThanCondition(AbstractCondition): @@ -292,8 +296,7 @@ def __init__( parent: Hyperparameter, value: str | float | int, ) -> None: - """ - Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter + """Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *less than* ``value``. Make *b* an active hyperparameter if *a* is less than 5 @@ -319,15 +322,14 @@ def __init__( Value, which the parent is compared to """ super().__init__(child, parent) - self.parent.allow_greater_less_comparison() - if not parent.is_legal(value): + if not parent.legal_value(value): raise ValueError( f"Hyperparameter '{child.name}' is " f"conditional on the illegal value '{value}' of " f"its parent hyperparameter '{parent.name}'", ) self.value = value - self.vector_value = self.parent._inverse_transform(self.value) + self.vector_value = self.parent.to_vector(self.value) def __repr__(self) -> str: return f"{self.child.name} | {self.parent.name} < {self.value!r}" @@ -340,16 +342,18 @@ def __copy__(self): ) def _evaluate(self, value: str | float | int) -> bool: - if not self.parent.is_legal(value): + if not self.parent.legal_value(value): return False - return self.parent.compare(value, self.value) is Comparison.LESS_THAN + return self.parent.compare_value(value, self.value) is Comparison.LESS_THAN def _inner_evaluate_vector(self, value) -> bool: - if not self.parent.is_legal_vector(value): + if not self.parent.legal_vector(value): return False - return self.parent.compare_vector(value, self.vector_value) is Comparison.LESS_THAN + return ( + self.parent.compare_vector(value, self.vector_value) is Comparison.LESS_THAN + ) class GreaterThanCondition(AbstractCondition): @@ -359,8 +363,7 @@ def __init__( parent: Hyperparameter, value: str | float | int, ) -> None: - """ - Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter + """Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *greater than* ``value``. Make *b* an active hyperparameter if *a* is greater than 5 @@ -387,15 +390,19 @@ def __init__( """ super().__init__(child, parent) - self.parent.allow_greater_less_comparison() - if not parent.is_legal(value): + if not self.parent.orderable: + raise ValueError( + f"The parent hyperparameter {self.parent} must be orderable to use " + "GreaterThanCondition", + ) + if not parent.legal_value(value): raise ValueError( f"Hyperparameter '{child.name}' is " f"conditional on the illegal value '{value}' of " f"its parent hyperparameter '{parent.name}'", ) self.value = value - self.vector_value = self.parent._inverse_transform(self.value) + self.vector_value = self.parent.to_vector(self.value) def __repr__(self) -> str: return f"{self.child.name} | {self.parent.name} > {self.value!r}" @@ -408,16 +415,19 @@ def __copy__(self): ) def _evaluate(self, value: None | str | float | int) -> bool: - if not self.parent.is_legal(value): + if not self.parent.legal_value(value): return False - return self.parent.compare(value, self.value) is Comparison.GREATER_THAN + return self.parent.compare_value(value, self.value) is Comparison.GREATER_THAN def _inner_evaluate_vector(self, value) -> int: - if not self.parent.is_legal_vector(value): + if not self.parent.legal_vector(value): return False - return self.parent.compare_vector(value, self.vector_value) is Comparison.GREATER_THAN + return ( + self.parent.compare_vector(value, self.vector_value) + is Comparison.GREATER_THAN + ) class InCondition(AbstractCondition): @@ -427,8 +437,7 @@ def __init__( parent: Hyperparameter, values: list[str | float | int], ) -> None: - """ - Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter + """Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *in* a set of ``values``. make *b* an active hyperparameter if *a* is in the set [1, 2, 3, 4] @@ -456,7 +465,7 @@ def __init__( """ super().__init__(child, parent) for value in values: - if not parent.is_legal(value): + if not parent.legal_value(value): raise ValueError( f"Hyperparameter '{child.name}' is " f"conditional on the illegal value '{value}' of " @@ -464,7 +473,7 @@ def __init__( ) self.values = values self.value = values - self.vector_values = [self.parent._inverse_transform(value) for value in self.values] + self.vector_values = [self.parent.to_vector(value) for value in self.values] def __repr__(self) -> str: return "{} | {} in {{{}}}".format( @@ -499,11 +508,12 @@ def __init__(self, *args: AbstractCondition) -> None: children = self.get_children() for c1, c2 in combinations(children, 2): if c1 != c2: - raise ValueError("All Conjunctions and Conditions must have " "the same child.") + raise ValueError( + "All Conjunctions and Conditions must have " "the same child.", + ) def __eq__(self, other: Any) -> bool: - """ - This method implements a comparison between self and another + """Implements a comparison between self and another object. Additionally, it defines the __ne__() as stated in the @@ -520,7 +530,7 @@ def __eq__(self, other: Any) -> bool: if len(self.components) != len(other.components): return False - for component, other_component in zip(self.components, other.components): + for component, other_component in zip(self.components, other.components, strict=False): if component != other_component: return False @@ -530,7 +540,7 @@ def __copy__(self): return self.__class__(*[copy.copy(comp) for comp in self.components]) def get_descendant_literal_conditions(self) -> tuple[AbstractCondition]: - children = [] # type: List[AbstractCondition] + children = [] for component in self.components: if isinstance(component, AbstractConjunction): children.extend(component.get_descendant_literal_conditions()) @@ -555,13 +565,13 @@ def get_parents_vector(self) -> list[int]: return parents_vector def get_children(self) -> list[ConditionComponent]: - children = [] # type: List[ConditionComponent] + children = [] for component in self.components: children.extend(component.get_children()) return children def get_parents(self) -> list[ConditionComponent]: - parents = [] # type: List[ConditionComponent] + parents = [] for component in self.components: parents.extend(component.get_parents()) return parents @@ -661,8 +671,7 @@ def _evaluate(self, evaluations: np.ndarray) -> bool: class OrConjunction(AbstractConjunction): def __init__(self, *args: AbstractCondition) -> None: - """ - Similar to the *AndConjunction*, constraints can be combined by + """Similar to the *AndConjunction*, constraints can be combined by using the *OrConjunction*. >>> from ConfigSpace import ( diff --git a/ConfigSpace/configuration.py b/ConfigSpace/configuration.py index a9450e25..724982cd 100644 --- a/ConfigSpace/configuration.py +++ b/ConfigSpace/configuration.py @@ -1,7 +1,8 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING, Any, Iterator, KeysView, Mapping, Sequence +from collections.abc import Iterator, KeysView, Mapping, Sequence +from typing import TYPE_CHECKING, Any import numpy as np @@ -19,7 +20,7 @@ class Configuration(Mapping[str, Any]): def __init__( self, configuration_space: ConfigurationSpace, - values: Mapping[str, str | float | int | None] | None = None, + values: Mapping[str, Any] | None = None, vector: Sequence[float] | np.ndarray | None = None, allow_inactive_with_values: bool = False, origin: Any | None = None, @@ -37,27 +38,35 @@ def __init__( 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. + Args: + configuration_space: + The space this configuration is in + values: + A dictionary with pairs (hyperparameter_name, value), where value is + a legal value of the hyperparameter in the above configuration_space + vector: + A numpy array for efficient representation. Either values or vector + has to be given + allow_inactive_with_values: + 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: + Store information about the origin of this configuration. + Defaults to None. + config_id: + 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.") + 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 @@ -76,8 +85,8 @@ def __init__( 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 + # 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.empty(shape=len(configuration_space), dtype=float) @@ -88,7 +97,7 @@ def __init__( self._vector[i] = np.nan continue - if not hp.is_legal(value): + if not hp.legal_value(value): raise IllegalValueError(hp, value) # Truncate the float to be of constant length for a python version @@ -97,7 +106,7 @@ def __init__( value = float(repr(value)) self._values[key] = value - self._vector[i] = hp._inverse_transform(value) + self._vector[i] = hp.to_vector(value) # type: ignore self.is_valid_configuration() @@ -125,7 +134,7 @@ def __init__( def is_valid_configuration(self) -> None: """Check if the object is a valid. - Raises + Raises: ------ ValueError: If configuration is not valid. """ @@ -140,7 +149,7 @@ def get_array(self) -> np.ndarray: All continuous values are scaled between zero and one. - Returns + Returns: ------- numpy.ndarray The vector representation of the configuration @@ -155,13 +164,13 @@ def __contains__(self, item: object) -> bool: def __setitem__(self, key: str, value: Any) -> None: param = self.config_space[key] - if not param.is_legal(value): + if not param.legal_value(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) + vector_value = param.to_vector(value) new_array = c_util.change_hp_value( self.config_space, self.get_array().copy(), @@ -184,8 +193,8 @@ def __getitem__(self, key: str) -> Any: item_idx = self.config_space._hyperparameter_idx[key] - raw_value = self._vector[item_idx] - if not np.isfinite(raw_value): + vector = self._vector[item_idx] + if not np.isfinite(vector): # 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, @@ -194,7 +203,7 @@ def __getitem__(self, key: str) -> Any: raise KeyError(key) hyperparameter = self.config_space._hyperparameters[key] - value = hyperparameter._transform(raw_value) + value = hyperparameter.to_value(vector) # Truncate float to be of constant length for a python version if isinstance(hyperparameter, FloatHyperparameter): @@ -209,7 +218,7 @@ def __getitem__(self, key: str) -> Any: def keys(self) -> KeysView[str]: """Return the keys of the configuration. - Returns + Returns: ------- KeysView[str] The keys of the configuration @@ -247,12 +256,10 @@ def __len__(self) -> int: # make some other breaking changes # * Search `Marked Deprecated` to find others def get_dictionary(self) -> dict[str, Any]: - """A representation of the :class:`~ConfigSpace.configuration_space.Configuration` + """A representation of the `ConfigSpace.configuration_space.Configuration` in dictionary form. - Returns - ------- - dict + Retuns: Configuration as dictionary """ warnings.warn( diff --git a/ConfigSpace/configuration_space.py b/ConfigSpace/configuration_space.py index a95fb9cf..c9188f53 100644 --- a/ConfigSpace/configuration_space.py +++ b/ConfigSpace/configuration_space.py @@ -32,8 +32,9 @@ import io import warnings from collections import OrderedDict, defaultdict, deque +from collections.abc import Iterable, Iterator, KeysView, Mapping from itertools import chain -from typing import Any, Final, Iterable, Iterator, KeysView, Mapping, cast, overload +from typing import Any, Final, cast, overload import numpy as np @@ -72,6 +73,7 @@ UniformFloatHyperparameter, UniformIntegerHyperparameter, ) +from ConfigSpace.hyperparameters.hyperparameter import HyperparameterWithPrior _ROOT: Final = "__HPOlib_configuration_space_root__" @@ -103,7 +105,7 @@ def _parse_hyperparameters_from_dict(items: dict[str, Any]) -> Iterator[Hyperpar yield CategoricalHyperparameter(name, hp) # If it's an allowed type, it's a constant - elif isinstance(hp, (int, str, float)): + elif isinstance(hp, int | str | float): yield Constant(name, hp) else: @@ -119,16 +121,17 @@ def _assert_type(item: Any, expected: type, method: str | None = None) -> None: def _assert_legal(hyperparameter: Hyperparameter, value: tuple | list | Any) -> None: - if isinstance(value, (tuple, list)): + if isinstance(value, tuple | list): for v in value: - if not hyperparameter.is_legal(v): + if not hyperparameter.legal_value(v): raise IllegalValueError(hyperparameter, v) - elif not hyperparameter.is_legal(value): + elif not hyperparameter.legal_value(value): raise IllegalValueError(hyperparameter, value) class ConfigurationSpace(Mapping[str, Hyperparameter]): - """A collection-like object containing a set of hyperparameter definitions and conditions. + """A collection-like object containing a set of hyperparameter definitions and + conditions. A configuration space organizes all hyperparameters and its conditions as well as its forbidden clauses. Configurations can be sampled from @@ -151,13 +154,11 @@ def __init__( ] ) = None, ) -> None: - """ - - Parameters + """Parameters ---------- name : str | dict, optional - Name of the configuration space. If a dict is passed, this is considered the same - as the `space` arg. + Name of the configuration space. If a dict is passed, this is considered the + same as the `space` arg. seed : int, optional Random seed meta : dict, optional @@ -230,7 +231,7 @@ def add_hyperparameter(self, hyperparameter: Hyperparameter) -> Hyperparameter: hyperparameter : :ref:`Hyperparameters` The hyperparameter to add - Returns + Returns: ------- :ref:`Hyperparameters` The added hyperparameter @@ -255,7 +256,7 @@ def add_hyperparameters( hyperparameters : Iterable(:ref:`Hyperparameters`) Collection of hyperparameters to add - Returns + Returns: ------- list(:ref:`Hyperparameters`) List of added hyperparameters (same as input) @@ -289,7 +290,7 @@ def add_condition(self, condition: ConditionComponent) -> ConditionComponent: condition : :ref:`Conditions` Condition to add - Returns + Returns: ------- :ref:`Conditions` Same condition as input @@ -333,7 +334,7 @@ def add_conditions( conditions : list(:ref:`Conditions`) collection of conditions to add - Returns + Returns: ------- list(:ref:`Conditions`) Same as input conditions @@ -361,12 +362,11 @@ def add_conditions( else: raise TypeError(f"Unknown condition type {type(condition)}") - for edge, condition in zip(edges, conditions_to_add): + for edge, condition in zip(edges, conditions_to_add, strict=False): self._check_condition(edge[1], condition) self._check_edges(edges, values) - print(conditions_to_add) - for edge, condition in zip(edges, conditions_to_add): + for edge, condition in zip(edges, conditions_to_add, strict=False): self._add_edge(edge[0], edge[1], condition) self._sort_hyperparameters() @@ -377,15 +377,14 @@ def add_forbidden_clause( self, clause: AbstractForbiddenComponent, ) -> AbstractForbiddenComponent: - """ - Add a forbidden clause to the configuration space. + """Add a forbidden clause to the configuration space. Parameters ---------- clause : :ref:`Forbidden clauses` Forbidden clause to add - Returns + Returns: ------- :ref:`Forbidden clauses` Same as input forbidden clause @@ -400,15 +399,14 @@ def add_forbidden_clauses( self, clauses: list[AbstractForbiddenComponent], ) -> list[AbstractForbiddenComponent]: - """ - Add a list of forbidden clauses to the configuration space. + """Add a list of forbidden clauses to the configuration space. Parameters ---------- clauses : list(:ref:`Forbidden clauses`) Collection of forbidden clauses to add - Returns + Returns: ------- list(:ref:`Forbidden clauses`) Same as input clauses @@ -428,32 +426,33 @@ def add_configuration_space( delimiter: str = ":", parent_hyperparameter: dict | None = None, ) -> ConfigurationSpace: - """ - Combine two configuration space by adding one the other configuration + """Combine two configuration space by adding one the other configuration space. The contents of the configuration space, which should be added, are renamed to ``prefix`` + ``delimiter`` + old_name. - Parameters - ---------- - prefix : str - The prefix for the renamed hyperparameter | conditions | - forbidden clauses - configuration_space : :class:`~ConfigSpace.configuration_space.ConfigurationSpace` - The configuration space which should be added - delimiter : str, optional - Defaults to ':' - parent_hyperparameter : dict | None = None - Adds for each new hyperparameter the condition, that - ``parent_hyperparameter`` is active. Must be a dictionary with two keys - "parent" and "value", meaning that the added configuration space is active - when `parent` is equal to `value` - - Returns + Args: + prefix: + The prefix for the renamed hyperparameter | conditions | + forbidden clauses + configuration_space: + The configuration space which should be added + delimiter: + Defaults to ':' + parent_hyperparameter: + Adds for each new hyperparameter the condition, that + ``parent_hyperparameter`` is active. Must be a dictionary with two keys + "parent" and "value", meaning that the added configuration space is + active when `parent` is equal to `value` + + Returns: ------- - :class:`~ConfigSpace.configuration_space.ConfigurationSpace` - The configuration space, which was added + The configuration space, which was added. """ - _assert_type(configuration_space, ConfigurationSpace, method="add_configuration_space") + _assert_type( + configuration_space, + ConfigurationSpace, + method="add_configuration_space", + ) prefix_delim = f"{prefix}{delimiter}" @@ -524,7 +523,7 @@ def get_hyperparameter_by_idx(self, idx: int) -> str: idx : int Id of a hyperparameter - Returns + Returns: ------- str Name of the hyperparameter @@ -543,7 +542,7 @@ def get_idx_by_hyperparameter_name(self, name: str) -> int: name : str Name of a hyperparameter - Returns + Returns: ------- int Id of the hyperparameter with name ``name`` @@ -558,7 +557,7 @@ def get_idx_by_hyperparameter_name(self, name: str) -> int: def get_conditions(self) -> list[AbstractCondition]: """All conditions from the configuration space. - Returns + Returns: ------- list(:ref:`Conditions`) Conditions of the configuration space @@ -582,7 +581,7 @@ def get_conditions(self) -> list[AbstractCondition]: def get_forbiddens(self) -> list[AbstractForbiddenComponent]: """All forbidden clauses from the configuration space. - Returns + Returns: ------- list(:ref:`Forbidden clauses`) List with the forbidden clauses @@ -590,15 +589,14 @@ def get_forbiddens(self) -> list[AbstractForbiddenComponent]: return self.forbidden_clauses def get_children_of(self, name: str | Hyperparameter) -> list[Hyperparameter]: - """ - Return a list with all children of a given hyperparameter. + """Return a list with all children of a given hyperparameter. Parameters ---------- name : str, :ref:`Hyperparameters` Hyperparameter or its name, for which all children are requested - Returns + Returns: ------- list(:ref:`Hyperparameters`) Children of the hyperparameter @@ -634,8 +632,7 @@ def get_child_conditions_of( self, name: str | Hyperparameter, ) -> list[AbstractCondition]: - """ - Return a list with conditions of all children of a given + """Return a list with conditions of all children of a given hyperparameter referenced by its ``name``. Parameters @@ -643,7 +640,7 @@ def get_child_conditions_of( name : str, :ref:`Hyperparameters` Hyperparameter or its name, for which conditions are requested - Returns + Returns: ------- list(:ref:`Conditions`) List with the conditions on the children of the given hyperparameter @@ -663,7 +660,7 @@ def get_parents_of(self, name: str | Hyperparameter) -> list[Hyperparameter]: Can either be the name of a hyperparameter or the hyperparameter object. - Returns + Returns: ------- list[:ref:`Conditions`] List with all parent hyperparameters @@ -686,7 +683,7 @@ def get_parent_conditions_of( Can either be the name of a hyperparameter or the hyperparameter object - Returns + Returns: ------- list[:ref:`Conditions`] List with all conditions on parent hyperparameters @@ -701,7 +698,7 @@ def get_parent_conditions_of( def get_all_unconditional_hyperparameters(self) -> list[str]: """Names of unconditional hyperparameters. - Returns + Returns: ------- list[:ref:`Hyperparameters`] List with all parent hyperparameters, which are not part of a condition @@ -711,7 +708,7 @@ def get_all_unconditional_hyperparameters(self) -> list[str]: def get_all_conditional_hyperparameters(self) -> set[str]: """Names of all conditional hyperparameters. - Returns + Returns: ------- set[:ref:`Hyperparameters`] Set with all conditional hyperparameter @@ -721,7 +718,7 @@ def get_all_conditional_hyperparameters(self) -> set[str]: def get_default_configuration(self) -> Configuration: """Configuration containing hyperparameters with default values. - Returns + Returns: ------- :class:`~ConfigSpace.configuration_space.Configuration` Configuration with the set default values @@ -731,8 +728,7 @@ def get_default_configuration(self) -> Configuration: # For backward compatibility def check_configuration(self, configuration: Configuration) -> None: - """ - Check if a configuration is legal. Raises an error if not. + """Check if a configuration is legal. Raises an error if not. Parameters ---------- @@ -743,15 +739,18 @@ def check_configuration(self, configuration: Configuration) -> None: ConfigSpace.c_util.check_configuration(self, configuration.get_array(), False) def check_configuration_vector_representation(self, vector: np.ndarray) -> None: - """ - Raise error if configuration in vector representation is not legal. + """Raise error if configuration in vector representation is not legal. Parameters ---------- vector : np.ndarray Configuration in vector representation """ - _assert_type(vector, np.ndarray, method="check_configuration_vector_representation") + _assert_type( + vector, + np.ndarray, + method="check_configuration_vector_representation", + ) ConfigSpace.c_util.check_configuration(self, vector, False) def get_active_hyperparameters( @@ -765,7 +764,7 @@ def get_active_hyperparameters( configuration : :class:`~ConfigSpace.configuration_space.Configuration` Configuration for which the active hyperparameter are returned - Returns + Returns: ------- set(:class:`~ConfigSpace.configuration_space.Configuration`) The set of all active hyperparameter @@ -798,30 +797,27 @@ def get_active_hyperparameters( return active_hyperparameters @overload - def sample_configuration(self, size: None = None) -> Configuration: - ... + def sample_configuration(self, size: None = None) -> Configuration: ... # Technically this is wrong given the current behaviour but it's # sufficient for most cases. Once deprecation warning is up, # we can just have `1` always return a list of configurations # because an `int` was specified, `None` for single config. @overload - def sample_configuration(self, size: int) -> list[Configuration]: - ... + def sample_configuration(self, size: int) -> list[Configuration]: ... def sample_configuration( self, size: int | None = None, ) -> Configuration | list[Configuration]: - """ - Sample ``size`` configurations from the configuration space object. + """Sample ``size`` configurations from the configuration space object. Parameters ---------- size : int, optional Number of configurations to sample. Default to 1 - Returns + Returns: ------- :class:`~ConfigSpace.configuration_space.Configuration`, list[:class:`~ConfigSpace.configuration_space.Configuration`]: @@ -830,7 +826,8 @@ def sample_configuration( if size == 1: warnings.warn( "Please leave at default or explicitly set `size=None`." - " In the future, specifying a size will always retunr a list, even if 1", + " In the future, specifying a size will always retunr a list, even if" + " 1", DeprecationWarning, stacklevel=2, ) @@ -883,7 +880,10 @@ def sample_configuration( for i, hp_name in enumerate(self._hyperparameters): hyperparameter = self._hyperparameters[hp_name] - vector[:, i] = hyperparameter._sample(self.random, missing) + vector[:, i] = hyperparameter.sample_vector( + size=missing, + seed=self.random, + ) for i in range(missing): try: @@ -933,14 +933,14 @@ def remove_hyperparameter_priors(self) -> ConfigurationSpace: Non-uniform hyperpararmeters are replaced with uniform ones, and CategoricalHyperparameters with weights have their weights removed. - Returns + Returns: ------- :class:`~ConfigSpace.configuration_space.ConfigurationSpace` The resulting configuration space, without priors on the hyperparameters """ uniform_config_space = ConfigurationSpace() for parameter in self.values(): - if hasattr(parameter, "to_uniform"): + if isinstance(parameter, HyperparameterWithPrior): uniform_config_space.add_hyperparameter(parameter.to_uniform()) else: uniform_config_space.add_hyperparameter(copy.copy(parameter)) @@ -959,20 +959,17 @@ def remove_hyperparameter_priors(self) -> ConfigurationSpace: return uniform_config_space def estimate_size(self) -> float | int: - """Estimate the size of the current configuration space (i.e. unique configurations). - - This is ``np.inf`` in case if there is a single hyperparameter of size ``np.inf`` (i.e. a - :class:`~ConfigSpace.hyperparameters.UniformFloatHyperparameter`), otherwise - it is the product of the size of all hyperparameters. The function correctly guesses the - number of unique configurations if there are no condition and forbidden statements in the - configuration spaces. Otherwise, this is an upper bound. Use - :func:`~ConfigSpace.util.generate_grid` to generate all valid configurations if required. - - Returns - ------- - Union[float, int] + """Estimate the number of unique configurations. + + This is `np.inf` in case if there is a single hyperparameter of size `np.inf` + (i.e. a :class:`~ConfigSpace.hyperparameters.UniformFloatHyperparameter`), + otherwise it is the product of the size of all hyperparameters. The function + correctly guesses the number of unique configurations if there are no condition + and forbidden statements in the configuration spaces. Otherwise, this is an + upper bound. Use :func:`~ConfigSpace.util.generate_grid` to generate all + valid configurations if required. """ - sizes = [hp.get_size() for hp in self._hyperparameters.values()] + sizes = [hp.size for hp in self._hyperparameters.values()] if len(sizes) == 0: return 0.0 @@ -988,19 +985,19 @@ def substitute_hyperparameters_in_conditions( conditions: Iterable[ConditionComponent], new_configspace: ConfigurationSpace, ) -> list[ConditionComponent]: - """ - Takes a set of conditions and generates a new set of conditions with the same structure, - where each hyperparameter is replaced with its namesake in new_configspace. As such, the - set of conditions remain unchanged, but the included hyperparameters are changed to match - those types that exist in new_configspace. + """Takes a set of conditions and generates a new set of conditions with the same + structure, where each hyperparameter is replaced with its namesake in + new_configspace. As such, the set of conditions remain unchanged, but the + included hyperparameters are changed to match those types that exist in + new_configspace. Parameters ---------- new_configspace: ConfigurationSpace - A ConfigurationSpace containing hyperparameters with the same names as those in the - conditions. + A ConfigurationSpace containing hyperparameters with the same names as those + in the conditions. - Returns + Returns: ------- list[ConditionComponent]: The list of conditions, adjusted to fit the new ConfigurationSpace @@ -1010,9 +1007,11 @@ def substitute_hyperparameters_in_conditions( if isinstance(condition, AbstractConjunction): conjunction_type = type(condition) children = condition.get_descendant_literal_conditions() - substituted_children = ConfigurationSpace.substitute_hyperparameters_in_conditions( - children, - new_configspace, + substituted_children = ( + ConfigurationSpace.substitute_hyperparameters_in_conditions( + children, + new_configspace, + ) ) substituted_conjunction = conjunction_type(*substituted_children) new_conditions.append(substituted_conjunction) @@ -1040,12 +1039,15 @@ def substitute_hyperparameters_in_conditions( ) else: raise AttributeError( - f"Did not find the expected attribute in condition {type(condition)}.", + f"Did not find the expected attribute in condition" + f" {type(condition)}.", ) new_conditions.append(substituted_condition) else: - raise TypeError(f"Did not expect the supplied condition type {type(condition)}.") + raise TypeError( + f"Did not expect the supplied condition type {type(condition)}.", + ) return new_conditions @@ -1054,21 +1056,21 @@ def substitute_hyperparameters_in_forbiddens( forbiddens: Iterable[AbstractForbiddenComponent], new_configspace: ConfigurationSpace, ) -> list[AbstractForbiddenComponent]: - """ - Takes a set of forbidden clauses and generates a new set of forbidden clauses with the - same structure, where each hyperparameter is replaced with its namesake in new_configspace. - As such, the set of forbidden clauses remain unchanged, but the included hyperparameters are - changed to match those types that exist in new_configspace. + """Takes a set of forbidden clauses and generates a new set of forbidden clauses + with the same structure, where each hyperparameter is replaced with its + namesake in new_configspace. + As such, the set of forbidden clauses remain unchanged, but the included + hyperparameters are changed to match those types that exist in new_configspace. Parameters ---------- forbiddens: Iterable[AbstractForbiddenComponent] An iterable of forbiddens new_configspace: ConfigurationSpace - A ConfigurationSpace containing hyperparameters with the same names as those in the - forbidden clauses. + A ConfigurationSpace containing hyperparameters with the same names as those + in the forbidden clauses. - Returns + Returns: ------- list[AbstractForbiddenComponent]: The list of forbidden clauses, adjusted to fit the new ConfigurationSpace @@ -1078,9 +1080,11 @@ def substitute_hyperparameters_in_forbiddens( if isinstance(forbidden, AbstractForbiddenConjunction): conjunction_type = type(forbidden) children = forbidden.get_descendant_literal_clauses() - substituted_children = ConfigurationSpace.substitute_hyperparameters_in_forbiddens( - children, - new_configspace, + substituted_children = ( + ConfigurationSpace.substitute_hyperparameters_in_forbiddens( + children, + new_configspace, + ) ) substituted_conjunction = conjunction_type(*substituted_children) new_forbiddens.append(substituted_conjunction) @@ -1104,7 +1108,8 @@ def substitute_hyperparameters_in_forbiddens( ) else: raise AttributeError( - f"Did not find the expected attribute in forbidden {type(forbidden)}.", + f"Did not find the expected attribute in forbidden" + f" {type(forbidden)}.", ) new_forbiddens.append(substituted_forbidden) @@ -1146,9 +1151,6 @@ def __getitem__(self, key: str) -> Hyperparameter: return hp - # def __contains__(self, key: str) -> bool: - # return key in self._hyperparameters - def __repr__(self) -> str: retval = io.StringIO() retval.write("Configuration space object:\n Hyperparameters:\n") @@ -1160,7 +1162,11 @@ def __repr__(self) -> str: hyperparameters = sorted(self.values(), key=lambda t: t.name) # type: ignore if hyperparameters: retval.write(" ") - retval.write("\n ".join([str(hyperparameter) for hyperparameter in hyperparameters])) + retval.write( + "\n ".join( + [str(hyperparameter) for hyperparameter in hyperparameters], + ), + ) retval.write("\n") conditions = sorted(self.get_conditions(), key=lambda t: str(t)) @@ -1173,7 +1179,9 @@ def __repr__(self) -> str: if self.get_forbiddens(): retval.write(" Forbidden Clauses:\n") retval.write(" ") - retval.write("\n ".join([str(clause) for clause in self.get_forbiddens()])) + retval.write( + "\n ".join([str(clause) for clause in self.get_forbiddens()]), + ) retval.write("\n") retval.seek(0) @@ -1211,7 +1219,9 @@ def _add_hyperparameter(self, hyperparameter: Hyperparameter) -> None: # Save the index of each hyperparameter name to later on access a # vector of hyperparameter values by indices, must be done twice # because check_default_configuration depends on it - self._hyperparameter_idx.update({hp: i for i, hp in enumerate(self._hyperparameters)}) + self._hyperparameter_idx.update( + {hp: i for i, hp in enumerate(self._hyperparameters)}, + ) def _sort_hyperparameters(self) -> None: levels: OrderedDict[str, int] = OrderedDict() @@ -1361,7 +1371,7 @@ def _check_edges( edges: list[tuple[Hyperparameter, Hyperparameter]], values: list[Any], ) -> None: - for (parent, child), value in zip(edges, values): + for (parent, child), value in zip(edges, values, strict=False): # check if both nodes are already inserted into the graph if child.name not in self._hyperparameters: raise ChildNotFoundError(child, space=self) @@ -1399,8 +1409,12 @@ def _update_cache(self) -> None: self._child_conditions_of = { name: self._get_child_conditions_of(name) for name in self._hyperparameters } - self._parents_of = {name: self.get_parents_of(name) for name in self._hyperparameters} - self._children_of = {name: self.get_children_of(name) for name in self._hyperparameters} + self._parents_of = { + name: self.get_parents_of(name) for name in self._hyperparameters + } + self._children_of = { + name: self.get_children_of(name) for name in self._hyperparameters + } def _check_forbidden_component(self, clause: AbstractForbiddenComponent) -> None: _assert_type(clause, AbstractForbiddenComponent, "_check_forbidden_component") @@ -1416,12 +1430,17 @@ def _check_forbidden_component(self, clause: AbstractForbiddenComponent) -> None else: raise NotImplementedError(type(clause)) - def _check_hp(tmp_clause: AbstractForbiddenComponent, hp: Hyperparameter) -> None: + def _check_hp( + tmp_clause: AbstractForbiddenComponent, + hp: Hyperparameter, + ) -> None: if hp.name not in self._hyperparameters: raise HyperparameterNotFoundError( hp, space=self, - preamble=f"Cannot add '{tmp_clause}' because it references '{hp.name}'", + preamble=( + f"Cannot add '{tmp_clause}' because it references '{hp.name}'" + ), ) for tmp_clause in to_check: @@ -1449,7 +1468,7 @@ def _get_parents_of(self, name: str) -> list[Hyperparameter]: ---------- name : str - Returns + Returns: ------- list List with all parent hyperparameters @@ -1511,7 +1530,7 @@ def _check_configuration_rigorous( hp_value = vector[self._hyperparameter_idx[hp_name]] active = hp_name in active_hyperparameters - if not np.isnan(hp_value) and not hyperparameter.is_legal_vector(hp_value): + if not np.isnan(hp_value) and not hyperparameter.legal_vector(hp_value): raise IllegalValueError(hyperparameter, hp_value) if active and np.isnan(hp_value): @@ -1538,7 +1557,7 @@ def get_hyperparameter(self, name: str) -> Hyperparameter: name : str Name of the searched hyperparameter - Returns + Returns: ------- :ref:`Hyperparameters` Hyperparameter with the name ``name`` @@ -1553,7 +1572,7 @@ def get_hyperparameter(self, name: str) -> Hyperparameter: def get_hyperparameters(self) -> list[Hyperparameter]: """All hyperparameters in the space. - Returns + Returns: ------- list(:ref:`Hyperparameters`) A list with all hyperparameters stored in the configuration space object @@ -1568,7 +1587,7 @@ def get_hyperparameters(self) -> list[Hyperparameter]: def get_hyperparameters_dict(self) -> dict[str, Hyperparameter]: """All the ``(name, Hyperparameter)`` contained in the space. - Returns + Returns: ------- dict(str, :ref:`Hyperparameters`) An OrderedDict of names and hyperparameters @@ -1583,7 +1602,7 @@ def get_hyperparameters_dict(self) -> dict[str, Hyperparameter]: def get_hyperparameter_names(self) -> list[str]: """Names of all the hyperparameter in the space. - Returns + Returns: ------- list(str) List of hyperparameter names diff --git a/ConfigSpace/deprecate.py b/ConfigSpace/deprecate.py deleted file mode 100644 index db5bf338..00000000 --- a/ConfigSpace/deprecate.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -import warnings -from typing import Any - - -def deprecate( - thing: Any, - instead: str, - stacklevel: int = 3, -) -> None: - """Deprecate a thing and warn when it is used. - - Parameters - ---------- - thing : Any - The thing to deprecate. - - instead : str - What to use instead. - - stacklevel : int, optional - How many levels up in the stack to place the warning. - Defaults to 3. - """ - msg = f"{thing} is deprecated and will be removed in a future version." f"\n{instead}" - warnings.warn(msg, stacklevel=stacklevel) diff --git a/ConfigSpace/forbidden.pxd b/ConfigSpace/forbidden.pxd deleted file mode 100644 index b381a19b..00000000 --- a/ConfigSpace/forbidden.pxd +++ /dev/null @@ -1,24 +0,0 @@ -import numpy as np -cimport numpy as np - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t - - -cdef class AbstractForbiddenComponent(object): - - cdef public hyperparameter - cdef public int vector_id - cdef public value - cdef public DTYPE_t vector_value - - cdef int c_is_forbidden_vector(self, np.ndarray instantiated_hyperparameters, int strict) - cpdef get_descendant_literal_clauses(self) - cpdef set_vector_idx(self, hyperparameter_to_idx) - cpdef is_forbidden(self, instantiated_hyperparameters, strict) diff --git a/ConfigSpace/forbidden.py b/ConfigSpace/forbidden.py index fb1cb91d..ee3b4cb4 100644 --- a/ConfigSpace/forbidden.py +++ b/ConfigSpace/forbidden.py @@ -12,8 +12,8 @@ # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the nor the -# names of itConfigurationSpaces contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. +# names of itConfigurationSpaces contributors may be used to endorse or promote +# products derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -44,22 +44,13 @@ def __repr__(self): pass def __eq__(self, other: Any) -> bool: - """ - This method implements a comparison between self and another - object. - - Additionally, it defines the __ne__() as stated in the - documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result - unless it is NotImplemented. - - """ if not isinstance(other, self.__class__): return NotImplemented - return self.value == other.value and self.hyperparameter.name == other.hyperparameter.name + return ( + self.value == other.value + and self.hyperparameter.name == other.hyperparameter.name + ) def __hash__(self) -> int: """Override the default hash behavior (that returns the id or the object).""" @@ -80,14 +71,20 @@ def is_forbidden(self, instantiated_hyperparameters, strict): def is_forbidden_vector(self, instantiated_hyperparameters, strict): return bool(self.c_is_forbidden_vector(instantiated_hyperparameters, strict)) - def c_is_forbidden_vector(self, instantiated_hyperparameters: np.ndarray, strict: int) -> int: + def c_is_forbidden_vector( + self, + instantiated_hyperparameters: np.ndarray, + strict: int, + ) -> int: pass class AbstractForbiddenClause(AbstractForbiddenComponent): def __init__(self, hyperparameter: Hyperparameter): if not isinstance(hyperparameter, Hyperparameter): - raise TypeError("Argument 'hyperparameter' is not of type %s." % Hyperparameter) + raise TypeError( + "Argument 'hyperparameter' is not of type %s." % Hyperparameter, + ) self.hyperparameter = hyperparameter self.vector_id = -1 @@ -101,14 +98,14 @@ def set_vector_idx(self, hyperparameter_to_idx): class SingleValueForbiddenClause(AbstractForbiddenClause): def __init__(self, hyperparameter: Hyperparameter, value: Any) -> None: super().__init__(hyperparameter) - if not self.hyperparameter.is_legal(value): + if not self.hyperparameter.legal_value(value): raise ValueError( "Forbidden clause must be instantiated with a " f"legal hyperparameter value for '{self.hyperparameter}', but got " f"'{value!s}'", ) self.value = value - self.vector_value = self.hyperparameter._inverse_transform(self.value) + self.vector_value = self.hyperparameter.to_vector(self.value) def __copy__(self): return self.__class__( @@ -126,14 +123,17 @@ def is_forbidden(self, instantiated_hyperparameters, strict) -> int: "forbidden clause; you are missing " "'%s'" % self.hyperparameter.name, ) - else: - return False + return False return self._is_forbidden(value) - def c_is_forbidden_vector(self, instantiated_vector: np.ndarray, strict: int) -> int: + def c_is_forbidden_vector( + self, + instantiated_vector: np.ndarray, + strict: int, + ) -> int: value = instantiated_vector[self.vector_id] - if value != value: + if value != value: # noqa: PLR0124 if strict: raise ValueError( "Is_forbidden must be called with the " @@ -141,8 +141,7 @@ def c_is_forbidden_vector(self, instantiated_vector: np.ndarray, strict: int) -> "forbidden clause; you are missing " "'%s'" % self.vector_id, ) - else: - return False + return False return self._is_forbidden_vector(value) @@ -158,7 +157,7 @@ def __init__(self, hyperparameter: Hyperparameter, values: Any) -> None: super().__init__(hyperparameter) for value in values: - if not self.hyperparameter.is_legal(value): + if not self.hyperparameter.legal_value(value): raise ValueError( "Forbidden clause must be instantiated with a " f"legal hyperparameter value for '{self.hyperparameter}', but got " @@ -166,7 +165,7 @@ def __init__(self, hyperparameter: Hyperparameter, values: Any) -> None: ) self.values = values self.vector_values = [ - self.hyperparameter._inverse_transform(value) for value in self.values + self.hyperparameter.to_vector(value) for value in self.values ] def __copy__(self): @@ -180,8 +179,7 @@ def __eq__(self, other: Any) -> bool: return NotImplemented return ( - self.hyperparameter == other.hyperparameter - and self.values == other.values + self.hyperparameter == other.hyperparameter and self.values == other.values ) def is_forbidden(self, instantiated_hyperparameters, strict) -> bool: @@ -198,7 +196,11 @@ def is_forbidden(self, instantiated_hyperparameters, strict) -> bool: return self._is_forbidden(value) - def c_is_forbidden_vector(self, instantiated_vector: np.ndarray, strict: int) -> int: + def c_is_forbidden_vector( + self, + instantiated_vector: np.ndarray, + strict: int, + ) -> int: value = instantiated_vector[self.vector_id] if value != value: @@ -274,7 +276,7 @@ def __init__( >>> cs.add_forbidden_clause(forbidden_clause_a) Forbidden: a in {2, 3} - Note + Note: ---- The forbidden values have to be a subset of the hyperparameter's values. @@ -340,7 +342,9 @@ def __eq__(self, other: Any) -> bool: if self.n_components != other.n_components: return False - return all(self.components[i] == other.components[i] for i in range(self.n_components)) + return all( + self.components[i] == other.components[i] for i in range(self.n_components) + ) def set_vector_idx(self, hyperparameter_to_idx) -> None: for component in self.components: @@ -368,8 +372,7 @@ def is_forbidden(self, instantiated_hyperparameters, strict) -> bool: "you are (at least) missing " "'%s'" % dlc.hyperparameter.name, ) - else: - return False + return False values = np.empty(self.n_components, dtype=np.int32) @@ -383,7 +386,11 @@ def is_forbidden(self, instantiated_hyperparameters, strict) -> bool: return self._is_forbidden(self.n_components, values) - def c_is_forbidden_vector(self, instantiated_vector: np.ndarray, strict: int) -> int: + def c_is_forbidden_vector( + self, + instantiated_vector: np.ndarray, + strict: int, + ) -> int: e: int = 0 values = np.empty(self.n_components, dtype=np.int32) @@ -430,7 +437,7 @@ class ForbiddenAndConjunction(AbstractForbiddenConjunction): ---------- *args : list(:ref:`Forbidden clauses`) forbidden clauses, which should be combined - """ + """ # noqa: E501 def __repr__(self) -> str: retval = io.StringIO() @@ -450,7 +457,11 @@ def _is_forbidden(self, I: int, evaluations) -> int: return 0 return 1 - def c_is_forbidden_vector(self, instantiated_vector: np.ndarray, strict: int) -> int: + def c_is_forbidden_vector( + self, + instantiated_vector: np.ndarray, + strict: int, + ) -> int: # Copy from above to have early stopping of the evaluation of clauses - # gave only very modest improvements of ~5%; should probably be reworked # if adding more conjunctions in order to use better software design to @@ -513,8 +524,7 @@ def is_forbidden(self, instantiated_hyperparameters, strict): "forbidden clause; you are missing " "'%s'" % self.left.name, ) - else: - return False + return False if right is None: if strict: raise ValueError( @@ -523,15 +533,18 @@ def is_forbidden(self, instantiated_hyperparameters, strict): "forbidden clause; you are missing " "'%s'" % self.right.name, ) - else: - return False + return False return self._is_forbidden(left, right) def _is_forbidden(self, left, right) -> int: pass - def c_is_forbidden_vector(self, instantiated_vector: np.ndarray, strict: int) -> int: + def c_is_forbidden_vector( + self, + instantiated_vector: np.ndarray, + strict: int, + ) -> int: left = instantiated_vector[self.vector_ids[0]] right = instantiated_vector[self.vector_ids[1]] @@ -543,8 +556,7 @@ def c_is_forbidden_vector(self, instantiated_vector: np.ndarray, strict: int) -> "forbidden clause; you are missing " "'%s'" % self.vector_ids[0], ) - else: - return False + return False if right != right: if strict: @@ -554,11 +566,14 @@ def c_is_forbidden_vector(self, instantiated_vector: np.ndarray, strict: int) -> "forbidden clause; you are missing " "'%s'" % self.vector_ids[1], ) - else: - return False + return False - # Relation is always evaluated against actual value and not vector representation - return self._is_forbidden(self.left._transform(left), self.right._transform(right)) + # Relation is always evaluated against actual value and not vector + # representation + return self._is_forbidden( + self.left.to_value(left), + self.right.to_value(right), + ) def _is_forbidden_vector(self, left, right) -> int: pass @@ -577,7 +592,7 @@ class ForbiddenLessThanRelation(ForbiddenRelation): >>> cs.add_forbidden_clause(forbidden_clause) Forbidden: a < b - Note + Note: ---- If the values of the both hyperparameters are not comparible (e.g. comparing int and str), a TypeError is raised. For OrdinalHyperparameters @@ -615,7 +630,7 @@ class ForbiddenEqualsRelation(ForbiddenRelation): >>> cs.add_forbidden_clause(forbidden_clause) Forbidden: a == b - Note + Note: ---- If the values of the both hyperparameters are not comparible (e.g. comparing int and str), a TypeError is raised. For OrdinalHyperparameters @@ -652,7 +667,7 @@ class ForbiddenGreaterThanRelation(ForbiddenRelation): >>> cs.add_forbidden_clause(forbidden_clause) Forbidden: a > b - Note + Note: ---- If the values of the both hyperparameters are not comparible (e.g. comparing int and str), a TypeError is raised. For OrdinalHyperparameters diff --git a/ConfigSpace/functional.py b/ConfigSpace/functional.py index e8da4045..2d9a017e 100644 --- a/ConfigSpace/functional.py +++ b/ConfigSpace/functional.py @@ -1,8 +1,9 @@ from __future__ import annotations -from typing import Iterator +from collections.abc import Iterator import numpy as np +import numpy.typing as npt from more_itertools import roundrobin @@ -31,7 +32,7 @@ def center_range( step: int = 1 The step size - Returns + Returns: ------- Iterator[int] """ @@ -67,7 +68,7 @@ def arange_chunked( step: int = 1 The step size - Returns + Returns: ------- Iterator[np.ndarray] """ @@ -81,3 +82,128 @@ def arange_chunked( chunk_start = start + (chunk * chunk_size) chunk_stop = min(chunk_start + chunk_size, stop) yield np.arange(chunk_start, chunk_stop, step) + + +def split_arange( + *bounds: tuple[int | np.int64, int | np.int64], +) -> npt.NDArray[np.int64]: + """Split an arange into multiple ranges. + + >>> split_arange((0, 2), (3, 5), (6, 10)) + [0, 1, 3, 4, 6, 7, 8, 9] + + Parameters + ---------- + bounds: tuple[int, int] + The bounds of the ranges + + Returns: + The concatenated ranges + """ + return np.concatenate( + [np.arange(start, stop, dtype=int) for start, stop in bounds], + dtype=np.int64, + ) + + +def repr_maker(cls, **kwargs) -> str: + """Create a repr string for a class. + + >>> class A: + ... def __init__(self, a, b): + ... self.a = a + ... self.b = b + ... + ... def __repr__(self): + ... return repr(self, a=self.a, b=self.b) + ... + >>> A(1, 2) + A(a=1, b=2) + + Parameters + ---------- + cls: type + The class to create a repr for + + kwargs: dict + The kwargs to include in the repr + + Returns: + ------- + str + """ + return f"{cls.__name__}({', '.join(f'{k}={v!r}' for k, v in kwargs.items())})" + + +def in_bounds( + v: int | float | np.number, + bounds: tuple[int | float | np.number, int | float | np.number], + *, + integer: bool = False, +) -> bool: + """Check if a value is in bounds (inclusive). + + >>> in_bounds(5, 0, 10) + True + >>> in_bounds(5, 6, 10) + False + + Parameters + ---------- + v: int | float + The value to check + + low: int | float + The low end of the range + + high: int | float + The high end of the range + + Returns: + ------- + bool + """ + low, high = bounds + if integer: + return bool(low <= v <= high) and int(v) == v + + return bool(low <= v <= high) + + +def discretize( + x: npt.NDArray[np.float64], + *, + bounds: tuple[int | float | np.number, int | float | np.number], + bins: int, +) -> npt.NDArray[np.int64]: + """Discretize an array of values to their closest bin. + + Similar to `np.digitize` but does not require the bins to be specified or loaded + into memory. + Similar to `np.histogram` but returns the same length as the input array, where each + element is assigned to their integer bin. + + >>> discretize(np.array([0.0, 0.1, 0.3, 0.5, 1]), bounds=(0, 1), bins=3) + array([0, 0, 1, 1, 2]) + + Args: + x: np.NDArray[np.float64] + The values to discretize + + bounds: tuple[int, int] + The bounds of the range + + bins: int + The number of bins + + scale_back: bool = False + If `True` the discretized values will be scaled back to the original range + + Returns: + ------- + np.NDArray[np.int64] + """ + lower, upper = bounds + + norm = (x - lower) / (upper - lower) + return np.floor(norm * bins).clip(0, bins - 1) diff --git a/ConfigSpace/hyperparameters/__init__.py b/ConfigSpace/hyperparameters/__init__.py index 87e13f0a..ac60bdeb 100644 --- a/ConfigSpace/hyperparameters/__init__.py +++ b/ConfigSpace/hyperparameters/__init__.py @@ -3,28 +3,32 @@ from ConfigSpace.hyperparameters.categorical import CategoricalHyperparameter from ConfigSpace.hyperparameters.constant import Constant, UnParametrizedHyperparameter from ConfigSpace.hyperparameters.float_hyperparameter import FloatHyperparameter -from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter +from ConfigSpace.hyperparameters.hyperparameter import ( + Hyperparameter, + HyperparameterWithPrior, +) from ConfigSpace.hyperparameters.integer_hyperparameter import IntegerHyperparameter from ConfigSpace.hyperparameters.normal_float import NormalFloatHyperparameter from ConfigSpace.hyperparameters.normal_integer import NormalIntegerHyperparameter -from ConfigSpace.hyperparameters.numerical import NumericalHyperparameter +from ConfigSpace.hyperparameters.numerical_hyperparameter import NumericalHyperparameter from ConfigSpace.hyperparameters.ordinal import OrdinalHyperparameter from ConfigSpace.hyperparameters.uniform_float import UniformFloatHyperparameter from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter __all__ = [ - "Hyperparameter", - "Constant", - "UnParametrizedHyperparameter", - "OrdinalHyperparameter", + "BetaFloatHyperparameter", + "BetaIntegerHyperparameter", "CategoricalHyperparameter", - "NumericalHyperparameter", + "Constant", "FloatHyperparameter", + "Hyperparameter", + "HyperparameterWithPrior", "IntegerHyperparameter", - "UniformFloatHyperparameter", - "UniformIntegerHyperparameter", "NormalFloatHyperparameter", "NormalIntegerHyperparameter", - "BetaFloatHyperparameter", - "BetaIntegerHyperparameter", + "NumericalHyperparameter", + "OrdinalHyperparameter", + "UniformFloatHyperparameter", + "UniformIntegerHyperparameter", + "UnParametrizedHyperparameter", ] diff --git a/ConfigSpace/hyperparameters/_distributions.py b/ConfigSpace/hyperparameters/_distributions.py new file mode 100644 index 00000000..7d860183 --- /dev/null +++ b/ConfigSpace/hyperparameters/_distributions.py @@ -0,0 +1,410 @@ +# NOTE: Unfortunatly scipy.stats does not allow discrete distributions whose support +# is not integers, +# e.g. can't have a discrete distribution over [0.0, 1.0] with 10 bins. +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Protocol + +import numpy as np +import numpy.typing as npt +from scipy.stats import randint +from scipy.stats._discrete_distns import randint_gen + +from ConfigSpace.functional import arange_chunked, split_arange +from ConfigSpace.hyperparameters._hp_components import ( + DEFAULT_VECTORIZED_NUMERIC_STD, + ROUND_PLACES, + VDType, +) + +if TYPE_CHECKING: + from scipy.stats._distn_infrastructure import ( + rv_continuous_frozen, + rv_discrete_frozen, + ) + +# OPTIM: Some operations generate an arange which could blowup memory if +# done over the entire space of integers (int32/64). +# To combat this, `arange_chunked` is used in scenarios where reducion +# operations over all the elments could be done in partial steps independantly. +# For example, a sum over the pdf values could be done in chunks. +# This may add some small overhead for smaller ranges but is unlikely to +# be noticable. +ARANGE_CHUNKSIZE = 10_000_000 + +CONFIDENCE_FOR_NORMALIZATION_OF_DISCRETE = 0.999999 +NEIGHBOR_GENERATOR_N_RETRIES = 5 +NEIGHBOR_GENERATOR_SAMPLE_MULTIPLIER = 2 +RandomState = np.random.RandomState + + +class VectorDistribution(Protocol[VDType]): + lower: VDType + upper: VDType + + def max_density(self) -> float: ... + + def sample( + self, + n: int, + *, + seed: RandomState | None = None, + ) -> npt.NDArray[VDType]: ... + + def in_support(self, vector: VDType) -> bool: ... + + def pdf(self, vector: npt.NDArray[VDType]) -> npt.NDArray[np.float64]: ... + + +@dataclass +class DiscretizedContinuousScipyDistribution(VectorDistribution[np.float64]): + steps: int | np.int64 + dist: rv_continuous_frozen + max_density_value: float | None = None + normalization_constant_value: float | None = None + int_dist: randint_gen = field(init=False) + + def __post_init__(self): + int_gen = randint(0, self.steps) + assert isinstance(int_gen, randint_gen) + self.int_dist = int_gen + + @property + def lower(self) -> np.float64: + return self.dist.a + + @property + def upper(self) -> np.float64: + return self.dist.b + + def max_density(self) -> float: + if self.max_density_value is not None: + return self.max_density_value + + # Otherwise, we generate all possible integers and find the maximum + lower, upper = self._bounds_with_confidence() + lower_int, upper_int = self._as_integers(np.array([lower, upper])) + chunks = arange_chunked(lower_int, upper_int, chunk_size=ARANGE_CHUNKSIZE) + max_density = max( + self.pdf(self._rescale_integers(chunk)).max() for chunk in chunks + ) + self.max_density_value = max_density + return max_density + + def _rescale_integers( + self, + integers: npt.NDArray[np.int64], + ) -> npt.NDArray[np.float64]: + # the steps - 1 is because the range above is exclusive w.r.t. the upper bound + # e.g. + # Suppose setps = 5 + # then possible integers = [0, 1, 2, 3, 4] + # then possible values = [0, 0.25, 0.5, 0.75, 1] + # ... which span the 0, 1 range as intended + unit_normed = integers / (self.steps - 1) + return np.clip( + self.lower + unit_normed * (self.upper - self.lower), + self.lower, + self.upper, + ) + + def _as_integers(self, vector: npt.NDArray[np.float64]) -> npt.NDArray[np.int64]: + unit_normed = (vector - self.lower) / (self.upper - self.lower) + return np.rint(unit_normed * (self.steps - 1)).astype(int) + + def _bounds_with_confidence( + self, + confidence: float = CONFIDENCE_FOR_NORMALIZATION_OF_DISCRETE, + ) -> tuple[np.float64, np.float64]: + lower, upper = ( + self.dist.ppf((1 - confidence) / 2), + self.dist.ppf((1 + confidence) / 2), + ) + return max(lower, self.lower), min(upper, self.upper) + + def _normalization_constant(self) -> float: + if self.normalization_constant_value is not None: + return self.normalization_constant_value + + lower_int, upper_int = self._as_integers(np.array([self.lower, self.upper])) + # If there's a lot of possible values, we want to find + # the support for where the distribution is above some + # minimal pdf value, and only compute the normalization constant + # w.r.t. to those values. It is an approximation + if upper_int - lower_int > ARANGE_CHUNKSIZE: + l_bound, u_bound = self._bounds_with_confidence() + lower_int, upper_int = self._as_integers(np.array([l_bound, u_bound])) + + chunks = arange_chunked(lower_int, upper_int, chunk_size=ARANGE_CHUNKSIZE) + normalization_constant = sum( + self.dist.pdf(self._rescale_integers(chunk)).sum() for chunk in chunks + ) + self.normalization_constant_value = normalization_constant + return self.normalization_constant_value + + def sample( + self, + n: int, + *, + seed: RandomState | None = None, + ) -> npt.NDArray[np.float64]: + integers = self.int_dist.rvs(size=n, random_state=seed) + assert isinstance(integers, np.ndarray) + return self._rescale_integers(integers) + + def in_support(self, vector: np.float64) -> bool: + return self.pdf(np.array([vector]))[0] != 0 + + def pdf(self, vector: npt.NDArray[VDType]) -> npt.NDArray[np.float64]: + unit_normed = (vector - self.lower) / (self.upper - self.lower) + int_scaled = unit_normed * (self.steps - 1) + close_to_int = np.round(int_scaled, ROUND_PLACES) + rounded_as_int = np.rint(int_scaled) + + valid_entries = np.where( + (close_to_int == rounded_as_int) + & (vector >= self.lower) + & (vector <= self.upper), + rounded_as_int, + np.nan, + ) + + return self.dist.pdf(valid_entries) / self._normalization_constant() + + def neighborhood( + self, + vector: np.float64, + n: int, + *, + std: float | None = None, + seed: RandomState | None = None, + n_retries: int = NEIGHBOR_GENERATOR_N_RETRIES, + sample_multiplier: int = NEIGHBOR_GENERATOR_SAMPLE_MULTIPLIER, + ) -> npt.NDArray[np.float64]: + if std is None: + std = DEFAULT_VECTORIZED_NUMERIC_STD + + assert n < 1000000, "Can only generate less than 1 million neighbors." + seed = np.random.RandomState() if seed is None else seed + + center_int = self._as_integers(np.array([vector]))[0] + + # In the easiest case, the amount of neighbors we need is more than the amount + # possible, in this case, we can skip our sampling and just generate all + # neighbors, excluding the current value + if n >= self.steps - 1: + values = split_arange((0, center_int), (center_int, self.steps)) + return self._rescale_integers(values) + + # Otherwise, we use a repeated sampling strategy where we slowly increase the + # std of a normal, centered on `center`, slowly expanding `std` such that + # rejection won't fail. + + # We set up a buffer that can hold the number of neighbors we need, plus some + # extra excess from sampling, preventing us from having to reallocate memory. + SAMPLE_SIZE = n * sample_multiplier + BUFFER_SIZE = n * (sample_multiplier + 1) + neighbors = np.empty(BUFFER_SIZE, dtype=np.int64) + offset = 0 # Indexes into current progress of filling buffer + + # We extend the range of stds to try to find neighbors + stds = np.linspace(std, 1.0, n_retries + 1, endpoint=True) + + # The span over which we want std to cover + range_size = self.upper - self.lower + + for _std in stds: + # Generate candidates in vectorized space + candidates = seed.normal(vector, _std * range_size, size=SAMPLE_SIZE) + valid_candidates = candidates[ + (candidates >= self.lower) & (candidates <= self.upper) + ] + + # Transform to integers and get uniques + candidates_int = self._as_integers(valid_candidates) + + if offset == 0: + uniq = np.unique(candidates_int) + + n_candidates = len(uniq) + neighbors[:n_candidates] = uniq + offset += n_candidates + else: + uniq = np.unique(candidates_int) + new_uniq = np.setdiff1d(uniq, neighbors[:offset], assume_unique=True) + + n_new_unique = len(new_uniq) + neighbors[offset : offset + n_new_unique] = new_uniq + offset += n_new_unique + + # We have enough neighbors, we can stop and return the vectorized values + if offset >= n: + return self._rescale_integers(neighbors[:n]) + + raise ValueError( + f"Failed to find enough neighbors with {n_retries} retries." + f"Given {n} neighbors, we only found {offset}.", + f"The normal's for sampling neighbors were Normal({vector}, {list(stds)})" + f" which were meant to find neighbors of {vector}. in the range", + f" ({self.lower}, {self.upper}).", + ) + + +@dataclass +class ScipyDiscreteDistribution(VectorDistribution[VDType]): + rv: rv_discrete_frozen + max_density_value: float | Callable[[], float] + dtype: type[VDType] + + @property + def lower(self) -> VDType: + return self.rv.a + + @property + def upper(self) -> VDType: + return self.rv.b + + def sample( + self, + size: int | None = None, + *, + seed: RandomState | None = None, + ) -> npt.NDArray[VDType]: + return self.rv.rvs(size=size, random_state=seed).astype(self.dtype) + + def in_support(self, vector: VDType) -> bool: + return self.rv.a <= vector <= self.rv.b + + def max_density(self) -> float: + match self.max_density_value: + case float() | int(): + return self.max_density_value + case _: + max_density = self.max_density_value() + self.max_density_value = max_density + return max_density + + def pdf(self, vector: npt.NDArray[VDType]) -> npt.NDArray[np.float64]: + return self.rv.pmf(vector) + + +@dataclass +class ScipyContinuousDistribution(VectorDistribution[VDType]): + rv: rv_continuous_frozen + max_density_value: float | Callable[[], float] + dtype: type[VDType] + + @property + def lower(self) -> VDType: + return self.rv.a + + @property + def upper(self) -> VDType: + return self.rv.b + + def sample( + self, + size: int | None = None, + *, + seed: RandomState | None = None, + ) -> npt.NDArray[VDType]: + return self.rv.rvs(size=size, random_state=seed).astype(self.dtype) + + def in_support(self, vector: VDType) -> bool: + return self.rv.a <= vector <= self.rv.b + + def max_density(self) -> float: + match self.max_density_value: + case float() | int(): + return self.max_density_value + case _: + max_density = self.max_density_value() + self.max_density_value = max_density + return max_density + + def pdf(self, vector: npt.NDArray[VDType]) -> npt.NDArray[np.float64]: + return self.rv.pdf(vector) + + def neighborhood( + self, + vector: np.float64, + n: int, + *, + std: float | None = None, + seed: RandomState | None = None, + n_retries: int = NEIGHBOR_GENERATOR_N_RETRIES, + sample_multiplier: int = NEIGHBOR_GENERATOR_SAMPLE_MULTIPLIER, + ) -> npt.NDArray[np.float64]: + if std is None: + std = DEFAULT_VECTORIZED_NUMERIC_STD + + seed = np.random.RandomState() if seed is None else seed + + SAMPLE_SIZE = n * sample_multiplier + BUFFER_SIZE = n + n * sample_multiplier + neighbors = np.empty(BUFFER_SIZE, dtype=np.float64) + offset = 0 + + # We extend the range of stds to try to find neighbors + stds = np.linspace(std, 1.0, n_retries + 1, endpoint=True) + + # Generate batches of n * BUFFER_MULTIPLIER candidates, filling the above + # buffer until we have enough valid candidates. + # We should not overflow as the buffer + range_size = self.upper - self.lower + for _std in stds: + candidates = seed.normal(vector, _std * range_size, size=(SAMPLE_SIZE,)) + valid_candidates = candidates[ + (candidates >= self.lower) & (candidates <= self.upper) + ] + + n_candidates = len(valid_candidates) + neighbors[offset:n_candidates] = valid_candidates + offset += n_candidates + + # We have enough neighbors, we can stop and return the vectorized values + if offset >= n: + return neighbors[:n] + + raise ValueError( + f"Failed to find enough neighbors with {n_retries} retries." + f"Given {n} neighbors, we only found {offset}.", + f"The normal's for sampling neighbors were Normal({vector}, {list(stds)})" + f" which were meant to find neighbors of {vector}. in the range", + f" ({self.lower}, {self.upper}).", + ) + + +@dataclass +class ConstantVectorDistribution(VectorDistribution[np.integer]): + value: np.integer + + @property + def lower(self) -> np.integer: + return self.value + + @property + def upper(self) -> np.integer: + return self.value + + def max_density(self) -> float: + return 1.0 + + def sample( + self, + n: int | None = None, + *, + seed: RandomState | None = None, + ) -> np.integer | npt.NDArray[np.integer]: + if n is None: + return self.value + + return np.full((n,), self.value, dtype=np.integer) + + def in_support(self, vector: np.integer) -> bool: + return vector == self.value + + def pdf(self, vector: npt.NDArray[np.integer]) -> npt.NDArray[np.float64]: + return (vector == self.value).astype(float) diff --git a/ConfigSpace/hyperparameters/_hp_components.py b/ConfigSpace/hyperparameters/_hp_components.py new file mode 100644 index 00000000..55a44ca8 --- /dev/null +++ b/ConfigSpace/hyperparameters/_hp_components.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Protocol, TypeVar + +import numpy as np +import numpy.typing as npt + +from ConfigSpace.functional import split_arange + +RandomState = np.random.RandomState + +ROUND_PLACES = 9 +VECTORIZED_NUMERIC_LOWER = 0.0 +VECTORIZED_NUMERIC_UPPER = 1.0 +DEFAULT_VECTORIZED_NUMERIC_STD = 0.2 +CONSTANT_VECTOR_VALUE = np.int64(0) + +DType = TypeVar("DType", bound=np.number) +"""Type variable for the data type of the hyperparameter.""" + +VDType = TypeVar("VDType", bound=np.number) +"""Type variable for the data type of the vectorized hyperparameter.""" + +T_contra = TypeVar("T_contra", contravariant=True) + + +class _Transformer(Protocol[DType, VDType]): + def to_value(self, vector: npt.NDArray[VDType]) -> npt.NDArray[DType]: ... + + def to_vector( + self, + value: Sequence[DType] | npt.NDArray[DType], + ) -> npt.NDArray[VDType]: ... + + +class _Neighborhood(Protocol[VDType]): + def __call__( + self, + vector: VDType, + n: int, + *, + std: float | None = None, + seed: RandomState | None = None, + ) -> npt.NDArray[VDType]: ... + + +@dataclass +class TransformerSeq(_Transformer[DType, np.int64]): + seq: Sequence[DType] + + def to_value(self, vector: npt.NDArray[np.int64]) -> DType | Sequence[DType]: + return self.seq[vector] + + def to_vector( + self, + value: Sequence[DType] | npt.NDArray, + ) -> npt.NDArray[np.int64]: + return np.array([self.seq.index(v) for v in value], dtype=np.int64) + + +class UnitScaler(_Transformer[DType, np.float64]): + def __init__( + self, + low: float | int | np.number, + high: float | int | np.number, + *, + log: bool = False, + ): + if low >= high: + raise ValueError( + f"Upper bound {high:f} must be larger than lower bound {low:f}", + ) + + if log and low <= 0: + raise ValueError( + f"Negative lower bound {low:f} for log-scale is not possible.", + ) + + self.low = low + self.high = high + self.log = log + self.diff = high - low + + def to_value( + self, + vector: npt.NDArray[np.float64], + ) -> npt.NDArray[DType]: + """Transform a value from the unit interval to the range.""" + # linear (0-1) space to log scaling (0-1) + if self.log: + log_low = np.log(self.low) + log_high = np.log(self.high) + x = vector * (log_high - log_low) + log_low + x = np.exp(x) + else: + x = vector * (self.high - self.low) + self.low + + return np.clip(x, self.low, self.high, dtype=np.float64) + + def to_vector(self, value: npt.NDArray[DType]) -> npt.NDArray[np.float64]: + """Transform a value from the range to the unit interval.""" + x = value + if self.log: + x = (np.log(x) - np.log(self.low)) / (np.log(self.high) - np.log(self.low)) + else: + x = (x - self.low) / (self.high - self.low) + + return np.clip(x, 0.0, 1.0, dtype=np.float64) + + +@dataclass +class NeighborhoodCat(_Neighborhood[np.int64]): + n: int + + def __call__( + self, + vector: np.int64, + n: int, + *, + std: float | None = None, # noqa: ARG002 + seed: RandomState | None = None, + ) -> npt.NDArray[np.int64]: + choices = split_arange((0, vector), (vector, self.n)) + seed = np.random.RandomState() if seed is None else seed + return seed.choice(choices, n, replace=False) + + +def ordinal_neighborhood( + vector: np.int64, + n: int, + *, + size: int, + std: float | None = None, + seed: RandomState | None = None, +) -> npt.NDArray[np.int64]: + end_index = size - 1 + assert 0 <= vector <= end_index + + # No neighbors if it's the only element + if size == 1: + return np.array([]) + + # We have at least 2 elements + if vector == 0: + return np.array([1]) + + if vector == end_index: + return np.array([end_index - 1]) + + # We have at least 3 elements and the value is not at the ends + neighbors = np.array([vector - 1, vector + 1]) + if n == 1: + seed = np.random.RandomState() if seed is None else seed + return np.array([seed.choice(neighbors)]) + + return neighbors + + +@dataclass +class TransformerConstant(_Transformer[DType, np.integer]): + value: DType + + def to_vector( + self, + value: DType | npt.NDArray[DType], + ) -> np.integer | npt.NDArray[np.integer]: + if isinstance(value, np.ndarray | Sequence): + return np.full_like(value, CONSTANT_VECTOR_VALUE, dtype=np.integer) + + return CONSTANT_VECTOR_VALUE + + def to_value( + self, + vector: np.integer | npt.NDArray[np.integer], + ) -> DType | npt.NDArray[DType]: + if isinstance(vector, np.ndarray): + try: + return np.full_like(vector, self.value, dtype=type(self.value)) + except TypeError: + # Let numpy figure it out + return np.array([self.value] * len(vector)) + + return self.value diff --git a/ConfigSpace/hyperparameters/beta_float.pxd b/ConfigSpace/hyperparameters/beta_float.pxd deleted file mode 100644 index 73f62770..00000000 --- a/ConfigSpace/hyperparameters/beta_float.pxd +++ /dev/null @@ -1,19 +0,0 @@ -import numpy as np -cimport numpy as np -np.import_array() - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t - -from .uniform_float cimport UniformFloatHyperparameter - - -cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): - cdef public alpha - cdef public beta diff --git a/ConfigSpace/hyperparameters/beta_float.py b/ConfigSpace/hyperparameters/beta_float.py index 1680eef7..560edaed 100644 --- a/ConfigSpace/hyperparameters/beta_float.py +++ b/ConfigSpace/hyperparameters/beta_float.py @@ -1,34 +1,41 @@ from __future__ import annotations -import io -from typing import TYPE_CHECKING, Any +from collections.abc import Hashable, Mapping +from typing import Any, ClassVar import numpy as np from scipy.stats import beta as spbeta +from ConfigSpace.hyperparameters._distributions import ( + ScipyContinuousDistribution, +) +from ConfigSpace.hyperparameters._hp_components import ROUND_PLACES, UnitScaler +from ConfigSpace.hyperparameters.beta_integer import BetaIntegerHyperparameter +from ConfigSpace.hyperparameters.float_hyperparameter import FloatHyperparameter +from ConfigSpace.hyperparameters.hyperparameter import HyperparameterWithPrior from ConfigSpace.hyperparameters.uniform_float import UniformFloatHyperparameter -if TYPE_CHECKING: - from ConfigSpace.hyperparameters.beta_integer import BetaIntegerHyperparameter +class BetaFloatHyperparameter( + FloatHyperparameter, + HyperparameterWithPrior[UniformFloatHyperparameter], +): + orderable: ClassVar[bool] = True -class BetaFloatHyperparameter(UniformFloatHyperparameter): def __init__( self, name: str, - alpha: int | float, - beta: int | float, - lower: float | int, - upper: float | int, - default_value: None | float = None, - q: int | float | None = None, + alpha: int | float | np.number, + beta: int | float | np.number, + lower: float | int | np.number, + upper: float | int | np.number, + default_value: None | float | int | np.number = None, log: bool = False, - meta: dict | None = None, + meta: Mapping[Hashable, Any] | None = None, ) -> None: - r""" - A beta distributed float hyperparameter. The 'lower' and 'upper' parameters move the - distribution from the [0, 1]-range and scale it appropriately, but the shape of the - distribution is preserved as if it were in [0, 1]-range. + r"""A beta distributed float hyperparameter. The 'lower' and 'upper' parameters move + the distribution from the [0, 1]-range and scale it appropriately, but the shape + of the distribution is preserved as if it were in [0, 1]-range. Its values are sampled from a beta distribution :math:`Beta(\alpha, \beta)`. @@ -38,237 +45,100 @@ def __init__( >>> BetaFloatHyperparameter('b', alpha=3, beta=2, lower=1, upper=4, log=False) b, Type: BetaFloat, Alpha: 3.0 Beta: 2.0, Range: [1.0, 4.0], Default: 3.0 - Parameters - ---------- - name : str - Name of the hyperparameter, with which it can be accessed - alpha : int, float - Alpha parameter of the normalized beta distribution - beta : int, float - Beta parameter of the normalized beta distribution - lower : int, float - Lower bound of a range of values from which the hyperparameter will be sampled. - The Beta disribution gets scaled by the total range of the hyperparameter. - upper : int, float - Upper bound of a range of values from which the hyperparameter will be sampled. - The Beta disribution gets scaled by the total range of the hyperparameter. - default_value : int, float, optional - Sets the default value of a hyperparameter to a given value - q : int, float, optional - Quantization factor - log : bool, optional - If ``True``, the values of the hyperparameter will be sampled - on a logarithmic scale. Default to ``False`` - meta : Dict, optional - Field for holding meta data provided by the user. - Not used by the configuration space. + Args: + name: + Name of the hyperparameter, with which it can be accessed + alpha: + Alpha parameter of the normalized beta distribution + beta: + Beta parameter of the normalized beta distribution + lower: + Lower bound of a range of values from which the hyperparameter will be + sampled. The Beta disribution gets scaled by the total range of the + hyperparameter. + upper: + Upper bound of a range of values from which the hyperparameter will be + sampled. The Beta disribution gets scaled by the total range of the + hyperparameter. + default_value: + Sets the default value of a hyperparameter to a given value + log: + If `True` the values of the hyperparameter will be sampled + on a logarithmic scale. Default to `False` + meta: + Field for holding meta data provided by the user. + Not used by the configuration space. """ - # TODO - we cannot use the check_default of UniformFloat (but everything else), - # but we still need to overwrite it. Thus, we first just need it not to raise an - # error, which we do by setting default_value = upper - lower / 2 to not raise an error, - # then actually call check_default once we have alpha and beta, and are not inside - # UniformFloatHP. - super().__init__( - name=name, - lower=lower, - upper=upper, - default_value=(upper + lower) / 2, - q=q, - log=log, - meta=meta, - ) - self.alpha = float(alpha) - self.beta = float(beta) if (alpha < 1) or (beta < 1): raise ValueError( "Please provide values of alpha and beta larger than or equal to" " 1 so that the probability density is finite.", ) - if (self.q is not None) and self.log and (default_value is None): - raise ValueError( - "Logscale and quantization together results in incorrect default values. " - "We recommend specifying a default value manually for this specific case.", - ) - - self.default_value = self.check_default(default_value) - self.normalized_default_value = self._inverse_transform(self.default_value) - - def __repr__(self) -> str: - repr_str = io.StringIO() - repr_str.write( - "{}, Type: BetaFloat, Alpha: {} Beta: {}, Range: [{}, {}], Default: {}".format( - self.name, - repr(self.alpha), - repr(self.beta), - repr(self.lower), - repr(self.upper), - repr(self.default_value), - ), - ) + self.alpha = np.float64(alpha) + self.beta = np.float64(beta) + self.lower = np.float64(lower) + self.upper = np.float64(upper) + self.log = bool(log) - if self.log: - repr_str.write(", on log-scale") - if self.q is not None: - repr_str.write(", Q: %s" % str(self.q)) - repr_str.seek(0) - return repr_str.getvalue() + try: + scaler = UnitScaler(self.lower, self.upper, log=log) + except ValueError as e: + raise ValueError(f"Hyperparameter '{name}' has illegal settings") from e - def __eq__(self, other: Any) -> bool: - """ - This method implements a comparison between self and another - object. + beta_rv = spbeta(self.alpha, self.beta) + if (self.alpha > 1) or (self.beta > 1): + normalized_mode = (self.alpha - 1) / (self.alpha + self.beta - 2) + else: + # If both alpha and beta are 1, we have a uniform distribution. + normalized_mode = 0.5 - Additionally, it defines the __ne__() as stated in the - documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result - unless it is NotImplemented. + max_density_value = float(beta_rv.pdf(normalized_mode)) # type: ignore - """ - if not isinstance(other, self.__class__): - return False + if default_value is None: + _default_value = np.float64(scaler.to_value(np.array([normalized_mode]))[0]) + else: + _default_value = np.float64(np.round(default_value, ROUND_PLACES)) - return ( - self.name == other.name - and self.default_value == other.default_value - and self.alpha == other.alpha - and self.beta == other.beta - and self.log == other.log - and self.q == other.q - and self.lower == other.lower - and self.upper == other.upper + vector_dist = ScipyContinuousDistribution( + rv=spbeta(self.alpha, self.beta), # type: ignore + max_density_value=max_density_value, + dtype=np.float64, ) - def __copy__(self): - return BetaFloatHyperparameter( - name=self.name, - default_value=self.default_value, - alpha=self.alpha, - beta=self.beta, - log=self.log, - q=self.q, - lower=self.lower, - upper=self.upper, - meta=self.meta, + super().__init__( + name=name, + size=np.inf, + default_value=_default_value, + meta=meta, + transformer=scaler, + vector_dist=vector_dist, + neighborhood=vector_dist.neighborhood, + neighborhood_size=np.inf, ) - def __hash__(self): - return hash((self.name, self.alpha, self.beta, self.lower, self.upper, self.log, self.q)) - def to_uniform(self) -> UniformFloatHyperparameter: return UniformFloatHyperparameter( - self.name, - self.lower, - self.upper, + name=self.name, + lower=self.lower, + upper=self.upper, default_value=self.default_value, - q=self.q, log=self.log, - meta=self.meta, + meta=None, ) - def check_default(self, default_value: int | float | None) -> int | float: - # return mode as default - # TODO - for log AND quantization together specifially, this does not give the exact right - # value, due to the bounds _lower and _upper being adjusted when quantizing in - # UniformFloat. - if default_value is None: - if (self.alpha > 1) or (self.beta > 1): - normalized_mode = (self.alpha - 1) / (self.alpha + self.beta - 2) - else: - # If both alpha and beta are 1, we have a uniform distribution. - normalized_mode = 0.5 - - ub = self._inverse_transform(self.upper) - lb = self._inverse_transform(self.lower) - scaled_mode = normalized_mode * (ub - lb) + lb - return self._transform_scalar(scaled_mode) - - elif self.is_legal(default_value): - return default_value - else: - raise ValueError("Illegal default value %s" % str(default_value)) - def to_integer(self) -> BetaIntegerHyperparameter: - q_int = None if self.q is None else int(np.rint(self.q)) - lower = int(np.ceil(self.lower)) upper = int(np.floor(self.upper)) default_value = int(np.rint(self.default_value)) - from ConfigSpace.hyperparameters.beta_integer import BetaIntegerHyperparameter return BetaIntegerHyperparameter( - self.name, + name=self.name, lower=lower, upper=upper, - alpha=self.alpha, - beta=self.beta, default_value=default_value, - q=q_int, log=self.log, + meta=None, + alpha=float(self.alpha), + beta=float(self.beta), ) - - def is_legal(self, value: float) -> bool: - if isinstance(value, (float, int)): - return self.upper >= value >= self.lower - return False - - def is_legal_vector(self, value) -> bool: - return self._upper >= value >= self._lower - - def _sample( - self, - rs: np.random.RandomState, - size: int | None = None, - ) -> np.ndarray | float: - alpha = self.alpha - beta = self.beta - return spbeta(alpha, beta).rvs(size=size, random_state=rs) - - def _pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal - distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - ub = self._inverse_transform(self.upper) - lb = self._inverse_transform(self.lower) - alpha = self.alpha - beta = self.beta - return ( - spbeta(alpha, beta, loc=lb, scale=ub - lb).pdf(vector) - * (ub - lb) - / (self._upper - self._lower) - ) - - def get_max_density(self) -> float: - if (self.alpha > 1) or (self.beta > 1): - normalized_mode = (self.alpha - 1) / (self.alpha + self.beta - 2) - elif self.alpha < self.beta: - normalized_mode = 0 - elif self.alpha > self.beta: - normalized_mode = 1 - else: - normalized_mode = 0.5 - - ub = self._inverse_transform(self.upper) - lb = self._inverse_transform(self.lower) - scaled_mode = normalized_mode * (ub - lb) + lb - - # Since _pdf takes only a numpy array, we have to create the array, - # and retrieve the element in the first (and only) spot in the array - return self._pdf(np.array([scaled_mode]))[0] diff --git a/ConfigSpace/hyperparameters/beta_integer.pxd b/ConfigSpace/hyperparameters/beta_integer.pxd deleted file mode 100644 index 11319cf0..00000000 --- a/ConfigSpace/hyperparameters/beta_integer.pxd +++ /dev/null @@ -1,21 +0,0 @@ -import numpy as np -cimport numpy as np -np.import_array() - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t - -from ConfigSpace.hyperparameters.uniform_integer cimport UniformIntegerHyperparameter - - -cdef class BetaIntegerHyperparameter(UniformIntegerHyperparameter): - cdef public alpha - cdef public beta - cdef public bfhp - cdef public normalization_constant diff --git a/ConfigSpace/hyperparameters/beta_integer.py b/ConfigSpace/hyperparameters/beta_integer.py index 25ed28d4..dfb8ca6d 100644 --- a/ConfigSpace/hyperparameters/beta_integer.py +++ b/ConfigSpace/hyperparameters/beta_integer.py @@ -1,26 +1,28 @@ from __future__ import annotations -import io -from typing import Any +from collections.abc import Hashable, Mapping +from typing import Any, ClassVar import numpy as np from scipy.stats import beta as spbeta -from ConfigSpace.functional import arange_chunked -from ConfigSpace.hyperparameters.beta_float import BetaFloatHyperparameter +from ConfigSpace.hyperparameters._distributions import ( + DiscretizedContinuousScipyDistribution, +) +from ConfigSpace.hyperparameters._hp_components import UnitScaler +from ConfigSpace.hyperparameters.hyperparameter import ( + HyperparameterWithPrior, +) +from ConfigSpace.hyperparameters.integer_hyperparameter import IntegerHyperparameter from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter -# OPTIM: Some operations generate an arange which could blowup memory if -# done over the entire space of integers (int32/64). -# To combat this, `arange_chunked` is used in scenarios where reducion -# operations over all the elments could be done in partial steps independantly. -# For example, a sum over the pdf values could be done in chunks. -# This may add some small overhead for smaller ranges but is unlikely to -# be noticable. -ARANGE_CHUNKSIZE = 10_000_000 +class BetaIntegerHyperparameter( + IntegerHyperparameter, + HyperparameterWithPrior[UniformIntegerHyperparameter], +): + orderable: ClassVar[bool] = True -class BetaIntegerHyperparameter(UniformIntegerHyperparameter): def __init__( self, name: str, @@ -29,14 +31,12 @@ def __init__( lower: int | float, upper: int | float, default_value: int | None = None, - q: None | int = None, log: bool = False, - meta: dict | None = None, + meta: Mapping[Hashable, Any] | None = None, ) -> None: - r""" - A beta distributed integer hyperparameter. The 'lower' and 'upper' parameters move the - distribution from the [0, 1]-range and scale it appropriately, but the shape of the - distribution is preserved as if it were in [0, 1]-range. + r"""A beta distributed integer hyperparameter. The 'lower' and 'upper' parameters + move the distribution from the [0, 1]-range and scale it appropriately, but the + shape of the distribution is preserved as if it were in [0, 1]-range. Its values are sampled from a beta distribution :math:`Beta(\alpha, \beta)`. @@ -47,201 +47,90 @@ def __init__( b, Type: BetaInteger, Alpha: 3.0 Beta: 2.0, Range: [1, 4], Default: 3 - Parameters - ---------- - name : str - Name of the hyperparameter with which it can be accessed - alpha : int, float - Alpha parameter of the distribution, from which hyperparameter is sampled - beta : int, float - Beta parameter of the distribution, from which - hyperparameter is sampled - lower : int, float - Lower bound of a range of values from which the hyperparameter will be sampled - upper : int, float - Upper bound of a range of values from which the hyperparameter will be sampled - default_value : int, optional - Sets the default value of a hyperparameter to a given value - q : int, optional - Quantization factor - log : bool, optional - If ``True``, the values of the hyperparameter will be sampled - on a logarithmic scale. Defaults to ``False`` - meta : Dict, optional - Field for holding meta data provided by the user. - Not used by the configuration space. - + Args: + name: + Name of the hyperparameter with which it can be accessed. + alpha: + Alpha parameter of the distribution, from which hyperparameter is + sampled. + beta: + Beta parameter of the distribution, from which + hyperparameter is sampled. + lower: + Lower bound of a range of values from which the hyperparameter will be + sampled. + upper: + Upper bound of a range of values from which the hyperparameter will be + sampled. + default_value: + Sets the default value of a hyperparameter to a given value. + log: + If `True` the values of the hyperparameter will be sampled + on a logarithmic scale. Defaults to `False`. + meta: + Field for holding meta data provided by the user. + Not used by the configuration space. """ - super().__init__( - name, - lower, - upper, - np.round((upper + lower) / 2), - q, - log, - meta, - ) - self.alpha = float(alpha) - self.beta = float(beta) if (alpha < 1) or (beta < 1): raise ValueError( "Please provide values of alpha and beta larger than or equal to" "1 so that the probability density is finite.", ) - q = 1 if self.q is None else self.q - self.bfhp = BetaFloatHyperparameter( - self.name, - self.alpha, - self.beta, - log=self.log, - q=q, - lower=self.lower, - upper=self.upper, - default_value=self.default_value, - ) - - self.default_value = self.check_default(default_value) - self.normalized_default_value = self._inverse_transform(self.default_value) - self.normalization_constant = self._compute_normalization() - - def __repr__(self) -> str: - repr_str = io.StringIO() - repr_str.write( - "{}, Type: BetaInteger, Alpha: {} Beta: {}, Range: [{}, {}], Default: {}".format( - self.name, - repr(self.alpha), - repr(self.beta), - repr(self.lower), - repr(self.upper), - repr(self.default_value), - ), - ) - - if self.log: - repr_str.write(", on log-scale") - if self.q is not None: - repr_str.write(", Q: %s" % str(self.q)) - repr_str.seek(0) - return repr_str.getvalue() + self.alpha = float(alpha) + self.beta = float(beta) + self.lower = np.int64(lower) + self.upper = np.int64(upper) + self.log = bool(log) + + try: + scaler = UnitScaler(self.lower, self.upper, log=log) + except ValueError as e: + raise ValueError(f"Hyperparameter '{name}' has illegal settings") from e + + beta_rv = spbeta(self.alpha, self.beta) + if (self.alpha > 1) or (self.beta > 1): + normalized_mode = (self.alpha - 1) / (self.alpha + self.beta - 2) + else: + # If both alpha and beta are 1, we have a uniform distribution. + normalized_mode = 0.5 - def __eq__(self, other: Any) -> bool: - """ - This method implements a comparison between self and another - object. + max_density_value = float(beta_rv.pdf(normalized_mode)) # type: ignore - Additionally, it defines the __ne__() as stated in the - documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result - unless it is NotImplemented. + if default_value is None: + _default_value = np.rint( + scaler.to_value(np.array([normalized_mode]))[0], + ).astype(np.int64) + else: + _default_value = np.rint(default_value).astype(np.int64) - """ - if not isinstance(other, self.__class__): - return False - - return ( - self.name == other.name - and self.alpha == other.alpha - and self.beta == other.beta - and self.log == other.log - and self.q == other.q - and self.lower == other.lower - and self.upper == other.upper + size = self.upper - self.lower + 1 + vector_dist = DiscretizedContinuousScipyDistribution( + rv=spbeta(self.alpha, self.beta), # type: ignore + max_density_value=max_density_value, + steps=size, ) - def __hash__(self): - return hash((self.name, self.alpha, self.beta, self.lower, self.upper, self.log, self.q)) + # Compute the normalization constant ahead of time + constant = vector_dist._normalization_constant() + vector_dist.normalization_constant_value = constant - def __copy__(self): - return BetaIntegerHyperparameter( - name=self.name, - default_value=self.default_value, - alpha=self.alpha, - beta=self.beta, - log=self.log, - q=self.q, - lower=self.lower, - upper=self.upper, - meta=self.meta, + super().__init__( + name=name, + size=int(size), + default_value=_default_value, + meta=meta, + transformer=scaler, + vector_dist=vector_dist, + neighborhood=vector_dist.neighborhood, + neighborhood_size=self._neighborhood_size, ) def to_uniform(self) -> UniformIntegerHyperparameter: return UniformIntegerHyperparameter( - self.name, - self.lower, - self.upper, + name=self.name, + lower=self.lower, + upper=self.upper, default_value=self.default_value, - q=self.q, log=self.log, - meta=self.meta, + meta=None, ) - - def check_default(self, default_value: int | float | None) -> int: - if default_value is None: - # Here, we just let the BetaFloat take care of the default value - # computation, and just tansform it accordingly - value = self.bfhp.check_default(None) - value = self._inverse_transform(value) - return self._transform(value) - - if self.is_legal(default_value): - return default_value - - raise ValueError(f"Illegal default value {default_value}") - - def _sample( - self, - rs: np.random.RandomState, - size: int | None = None, - ) -> np.ndarray | float: - value = self.bfhp._sample(rs, size=size) - # Map all floats which belong to the same integer value to the same - # float value by first transforming it to an integer and then - # transforming it back to a float between zero and one - value = self._transform(value) - return self._inverse_transform(value) - - def _compute_normalization(self): - if self.upper - self.lower > ARANGE_CHUNKSIZE: - a = self.bfhp._inverse_transform(self.lower) - b = self.bfhp._inverse_transform(self.upper) - confidence = 0.999999 - rv = spbeta(self.alpha, self.beta, loc=a, scale=b - a) - u, v = rv.ppf((1 - confidence) / 2), rv.ppf((1 + confidence) / 2) - lb = max(self.bfhp._transform(u), self.lower) - ub = min(self.bfhp._transform(v), self.upper + 1) - else: - lb = self.lower - ub = self.upper + 1 - - chunks = arange_chunked(lb, ub, chunk_size=ARANGE_CHUNKSIZE) - return sum(self.bfhp.pdf(chunk).sum() for chunk in chunks) - - def _pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal - distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). Optimally, an IntegerHyperparameter - should have a corresponding float, which can be utlized for the calls - to the probability density function (see e.g. NormalIntegerHyperparameter). - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - return self.bfhp._pdf(vector) / self.normalization_constant - - def get_max_density(self): - chunks = arange_chunked(self.lower, self.upper + 1, chunk_size=ARANGE_CHUNKSIZE) - maximum = max(self.bfhp.pdf(chunk).max() for chunk in chunks) - return maximum / self.normalization_constant diff --git a/ConfigSpace/hyperparameters/categorical.py b/ConfigSpace/hyperparameters/categorical.py index e6a25eae..41761066 100644 --- a/ConfigSpace/hyperparameters/categorical.py +++ b/ConfigSpace/hyperparameters/categorical.py @@ -1,118 +1,130 @@ from __future__ import annotations -import copy -import io from collections import Counter -from typing import Any, Sequence +from collections.abc import Hashable, Mapping, Sequence +from dataclasses import dataclass, field +from typing import Any, ClassVar import numpy as np +import numpy.typing as npt +from scipy.stats import rv_discrete -from ConfigSpace.hyperparameters.hyperparameter import Comparison, Hyperparameter +from ConfigSpace.hyperparameters._distributions import ScipyDiscreteDistribution +from ConfigSpace.hyperparameters._hp_components import ( + NeighborhoodCat, + TransformerSeq, +) +from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter -class CategoricalHyperparameter(Hyperparameter): - # TODO add more magic for automated type recognition - # TODO move from list to tuple for choices argument +@dataclass(init=False) +class CategoricalHyperparameter(Hyperparameter[Any, np.int64]): + orderable: ClassVar[bool] = False + choices: Sequence[Any] + weights: Sequence[int | float] | None + probabilities: npt.NDArray[np.float64] = field(repr=False, init=False) + def __init__( self, name: str, - choices: list[str | float | int] | tuple[float | int | str], - default_value: int | float | str | None = None, - meta: dict | None = None, + choices: Sequence[Any], weights: Sequence[int | float] | None = None, + default_value: Any | None = None, + meta: Mapping[Hashable, Any] | None = None, ) -> None: - """ - A categorical hyperparameter. - - Its values are sampled from a set of ``values``. - - ``None`` is a forbidden value, please use a string constant instead and parse - it in your own code, see `here _` - for further details. - - >>> from ConfigSpace import CategoricalHyperparameter - >>> - >>> CategoricalHyperparameter('c', choices=['red', 'green', 'blue']) - c, Type: Categorical, Choices: {red, green, blue}, Default: red - - Parameters - ---------- - name : str - Name of the hyperparameter, with which it can be accessed - choices : list or tuple with str, float, int - Collection of values to sample hyperparameter from - default_value : int, float, str, optional - Sets the default value of the hyperparameter to a given value - meta : Dict, optional - Field for holding meta data provided by the user. - Not used by the configuration space. - weights: Sequence[int | float] | None = None - List of weights for the choices to be used (after normalization) as - probabilities during sampling, no negative values allowed - """ - super().__init__(name, meta) # TODO check that there is no bullshit in the choices! + choices = list(choices) counter = Counter(choices) - for choice in choices: - if counter[choice] > 1: + for choice, count in counter.items(): + if count > 1: raise ValueError( - "Choices for categorical hyperparameters %s contain choice '%s' %d " - "times, while only a single oocurence is allowed." - % (name, choice, counter[choice]), + f"Choices for categorical hyperparameters {name} contain choice" + f" `{choice:!r}` {count} times, while only a single oocurence is" + " allowed.", ) - if choice is None: - raise TypeError("Choice 'None' is not supported") - if isinstance(choices, set): - raise TypeError( - "Using a set of choices is prohibited as it can result in " - "non-deterministic behavior. Please use a list or a tuple.", - ) - if isinstance(weights, set): - raise TypeError( - "Using a set of weights is prohibited as it can result in " - "non-deterministic behavior. Please use a list or a tuple.", + + match weights: + case set(): + raise TypeError( + "Using a set of weights is prohibited as it can result in " + "non-deterministic behavior. Please use a list or a tuple.", + ) + case Sequence(): + if len(weights) != len(choices): + raise ValueError( + "The list of weights and the list of choices are required to be" + f" of same length. Gave {len(weights)} weights and" + f" {len(choices)} choices.", + ) + if any(weight < 0 for weight in weights): + raise ValueError( + f"Negative weights are not allowed. Got {weights}.", + ) + if all(weight == 0 for weight in weights): + raise ValueError( + "All weights are zero, at least one weight has to be strictly" + " positive.", + ) + + if default_value is not None and default_value not in choices: + raise ValueError( + "The default value has to be one of the choices. " + f"Got {default_value!r} which is not in {choices}.", ) - self.choices = tuple(choices) - if weights is not None: - self.weights = tuple(weights) - else: - self.weights = None - self.probabilities = self._get_probabilities(choices=self.choices, weights=weights) - self.num_choices = len(choices) - self.choices_vector = list(range(self.num_choices)) - self._choices_set = set(self.choices_vector) - self.default_value = self.check_default(default_value) - self.normalized_default_value = self._inverse_transform(self.default_value) - def __repr__(self) -> str: - repr_str = io.StringIO() - repr_str.write("%s, Type: Categorical, Choices: {" % (self.name)) - for idx, choice in enumerate(self.choices): - repr_str.write(str(choice)) - if idx < len(self.choices) - 1: - repr_str.write(", ") - repr_str.write("}") - repr_str.write(", Default: ") - repr_str.write(str(self.default_value)) - # if the probability distribution is not uniform, write out the probabilities - if not np.all(self.probabilities == self.probabilities[0]): - repr_str.write(", Probabilities: %s" % str(self.probabilities)) - repr_str.seek(0) - return repr_str.getvalue() + if weights is None: + _weights = np.ones(len(choices), dtype=np.float64) + else: + _weights = np.asarray(weights, dtype=np.float64) + + probabilities = _weights / np.sum(_weights) + + self.choices = choices + self.probabilities = probabilities + + match default_value, weights: + case None, None: + default_value = choices[0] + case None, _: + default_value = choices[np.argmax(np.asarray(weights))] + case _ if default_value in choices: + default_value = default_value # noqa: PLW0127 + case _: + raise ValueError(f"Illegal default value {default_value}") + + size = len(choices) + custom_discrete = rv_discrete( + values=(np.arange(size), probabilities), + a=0, + b=size, + ).freeze() + vect_dist = ScipyDiscreteDistribution( + rv=custom_discrete, # type: ignore + max_density_value=1 / size, + dtype=np.int64, + ) - def __eq__(self, other: Any) -> bool: - """ - This method implements a comparison between self and another - object. + super().__init__( + name=name, + size=size, + default_value=default_value, + meta=meta, + transformer=TransformerSeq(seq=choices), + neighborhood=NeighborhoodCat(n=len(choices)), + vector_dist=vect_dist, + neighborhood_size=self._neighborhood_size, + ) - Additionally, it defines the __ne__() as stated in the - documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result - unless it is NotImplemented. + def to_uniform(self) -> CategoricalHyperparameter: + return CategoricalHyperparameter( + name=self.name, + choices=self.choices, + weights=None, + default_value=self.default_value, + meta=self.meta, + ) - """ + def __eq__(self, other: Any) -> bool: if not isinstance(other, self.__class__): return False @@ -122,6 +134,7 @@ def __eq__(self, other: Any) -> bool: } else: ordered_probabilities_self = None + if other.probabilities is not None: ordered_probabilities_other = { choice: ( @@ -134,269 +147,9 @@ def __eq__(self, other: Any) -> bool: else: ordered_probabilities_other = None - return ( - self.name == other.name - and set(self.choices) == set(other.choices) - and self.default_value == other.default_value - and ( - (ordered_probabilities_self is None and ordered_probabilities_other is None) - or ordered_probabilities_self == ordered_probabilities_other - or ( - ordered_probabilities_self is None - and len(np.unique(list(ordered_probabilities_other.values()))) == 1 - ) - or ( - ordered_probabilities_other is None - and len(np.unique(list(ordered_probabilities_self.values()))) == 1 - ) - ) - ) - - def __hash__(self): - return hash((self.name, self.choices)) - - def __copy__(self): - return CategoricalHyperparameter( - name=self.name, - choices=copy.deepcopy(self.choices), - default_value=self.default_value, - weights=copy.deepcopy(self.weights), - meta=self.meta, - ) - - def to_uniform(self) -> CategoricalHyperparameter: - """ - Creates a categorical parameter with equal weights for all choices - This is used for the uniform configspace when sampling configurations in the local search - in PiBO: https://openreview.net/forum?id=MMAeCXIa89. - - Returns - ------- - CategoricalHyperparameter - An identical parameter as the original, except that all weights are uniform. - """ - return CategoricalHyperparameter( - name=self.name, - choices=copy.deepcopy(self.choices), - default_value=self.default_value, - meta=self.meta, - ) - - def compare(self, value: int | float | str, value2: int | float | str) -> Comparison: - if value == value2: - return Comparison.EQUAL - - return Comparison.NOT_EQUAL - - def compare_vector(self, value: float, value2: float) -> Comparison: - if value == value2: - return Comparison.EQUAL - - return Comparison.NOT_EQUAL - - def is_legal(self, value: None | str | float | int) -> bool: - return value in self.choices - - def is_legal_vector(self, value) -> int: - return value in self._choices_set - - def _get_probabilities( - self, - choices: tuple[None | str | float | int], - weights: None | list[float], - ) -> None | list[float]: - if weights is None: - return tuple(np.ones(len(choices)) / len(choices)) - - if len(weights) != len(choices): - raise ValueError( - "The list of weights and the list of choices are required to be of same length.", - ) - - weights = np.array(weights) - - if np.all(weights == 0): - raise ValueError("At least one weight has to be strictly positive.") - - if np.any(weights < 0): - raise ValueError("Negative weights are not allowed.") - - return tuple(weights / np.sum(weights)) - - def check_default( - self, - default_value: None | str | float | int, - ) -> str | float | int: - if default_value is None: - return self.choices[np.argmax(self.weights) if self.weights is not None else 0] - elif self.is_legal(default_value): - return default_value - else: - raise ValueError("Illegal default value %s" % str(default_value)) - - def _sample( - self, - rs: np.random.RandomState, - size: int | None = None, - ) -> int | np.ndarray: - return rs.choice(a=self.num_choices, size=size, replace=True, p=self.probabilities) - - def _transform_vector(self, vector: np.ndarray) -> np.ndarray: - if np.isnan(vector).any(): - raise ValueError("Vector %s contains NaN's" % vector) - - if np.equal(np.mod(vector, 1), 0): - return self.choices[vector.astype(int)] - - raise ValueError( - "Can only index the choices of the ordinal " - f"hyperparameter {self} with an integer, but provided " - f"the following float: {vector:f}", - ) - - def _transform_scalar(self, scalar: float | int) -> float | int | str: - if scalar != scalar: - raise ValueError("Number %s is NaN" % scalar) - - if scalar % 1 == 0: - return self.choices[int(scalar)] - - raise ValueError( - "Can only index the choices of the ordinal " - f"hyperparameter {self} with an integer, but provided " - f"the following float: {scalar:f}", - ) - - def _transform( - self, - vector: np.ndarray | float | int | str, - ) -> np.ndarray | float | int | None: - try: - if isinstance(vector, np.ndarray): - return self._transform_vector(vector) - return self._transform_scalar(vector) - except ValueError: - return None - - def _inverse_transform(self, vector: None | str | float | int) -> int | float: - if vector is None: - return np.NaN - return self.choices.index(vector) - - def has_neighbors(self) -> bool: - return len(self.choices) > 1 - - def get_num_neighbors(self, value=None) -> int: - return len(self.choices) - 1 - - def get_neighbors( - self, - value: int, - rs: np.random.RandomState, - number: int | float = np.inf, - transform: bool = False, - ) -> list[float | int | str]: - neighbors = [] # type: List[Union[float, int, str]] - if number < len(self.choices): - while len(neighbors) < number: - rejected = True - index = int(value) - while rejected: - neighbor_idx = rs.randint(0, self.num_choices) - if neighbor_idx != index: - rejected = False - - candidate = self._transform(neighbor_idx) if transform else float(neighbor_idx) - - if candidate in neighbors: - continue - else: - neighbors.append(candidate) - else: - for candidate_idx, _candidate_value in enumerate(self.choices): - if int(value) == candidate_idx: - continue - else: - if transform: - candidate = self._transform(candidate_idx) - else: - candidate = float(candidate_idx) - - neighbors.append(candidate) - - return neighbors - - def allow_greater_less_comparison(self) -> bool: - raise ValueError( - "Parent hyperparameter in a > or < " - "condition must be a subclass of " - "NumericalHyperparameter or " - "OrdinalHyperparameter, but is " - "", - ) - - def pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the parameter in - the original parameter space (the one specified by the user). - For each parameter type, there is also a method _pdf which - operates on the transformed (and possibly normalized) parameter - space. Only legal values return a positive probability density, - otherwise zero. - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - # this check is to ensure shape is right (and np.shape does not work in cython) - if vector.ndim != 1: - raise ValueError("Method pdf expects a one-dimensional numpy array") - vector = np.array(self._inverse_transform(vector)) - return self._pdf(vector) - - def _pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal - distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). For categoricals, each vector gets - transformed to its corresponding index (but in float form). To be - able to retrieve the element corresponding to the index, the float - must be cast to int. - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - probs = np.array(self.probabilities) - nan = np.isnan(vector) - if np.any(nan): - # Temporarily pick any valid index to use `vector` as an index for `probs` - vector[nan] = 0 - res = np.array(probs[vector.astype(int)]) - if np.any(nan): - res[nan] = 0 - if res.ndim == 0: - return res.reshape(-1) - return res - - def get_max_density(self) -> float: - return np.max(self.probabilities) + return ordered_probabilities_self == ordered_probabilities_other - def get_size(self) -> float: - return len(self.choices) + def _neighborhood_size(self, value: Any | None) -> int: + if value is None or value not in self.choices: + return int(self.size) + return int(self.size) - 1 diff --git a/ConfigSpace/hyperparameters/constant.py b/ConfigSpace/hyperparameters/constant.py index 5a9c04a7..ff9d26c0 100644 --- a/ConfigSpace/hyperparameters/constant.py +++ b/ConfigSpace/hyperparameters/constant.py @@ -1,175 +1,60 @@ from __future__ import annotations -from typing import Any +from collections.abc import Hashable, Mapping +from typing import Any, ClassVar import numpy as np - +import numpy.typing as npt + +from ConfigSpace.hyperparameters._distributions import ConstantVectorDistribution +from ConfigSpace.hyperparameters._hp_components import ( + CONSTANT_VECTOR_VALUE, + DType, + TransformerConstant, +) from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter -class Constant(Hyperparameter): +def _empty_neighborhood(*_: Any, **__: Any) -> npt.NDArray[np.integer]: + return np.ndarray([], dtype=np.integer) + + +class Constant(Hyperparameter[DType, np.integer]): + orderable: ClassVar[bool] = False + def __init__( self, name: str, - value: str | int | float, - meta: dict | None = None, + value: DType, + meta: Mapping[Hashable, Any] | None = None, ) -> None: - """ - Representing a constant hyperparameter in the configuration space. + """Representing a constant hyperparameter in the configuration space. By sampling from the configuration space each time only a single, constant ``value`` will be drawn from this hyperparameter. - Parameters - ---------- - name : str - Name of the hyperparameter, with which it can be accessed - value : str, int, float - value to sample hyperparameter from - meta : Dict, optional - Field for holding meta data provided by the user. - Not used by the configuration space. + Args: + name: + Name of the hyperparameter, with which it can be accessed + value: + value to sample hyperparameter from + meta: + Field for holding meta data provided by the user. + Not used by the configuration space. """ - super().__init__(name, meta) - allowed_types = (int, float, str) - - if not isinstance(value, allowed_types) or isinstance(value, bool): - raise TypeError( - f"Constant value is of type {type(value)}, but only the " - f"following types are allowed: {allowed_types}", - ) # type: ignore - self.value = value - self.value_vector = 0.0 - self.default_value = value - self.normalized_default_value = 0.0 - - def __repr__(self) -> str: - repr_str = ["%s" % self.name, "Type: Constant", "Value: %s" % self.value] - return ", ".join(repr_str) - - def __eq__(self, other: Any) -> bool: - """ - This method implements a comparison between self and another - object. - Additionally, it defines the __ne__() as stated in the - documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result - unless it is NotImplemented. - - """ - if not isinstance(other, self.__class__): - return False - - return ( - self.value == other.value - and self.name == other.name - and self.default_value == other.default_value + super().__init__( + name=name, + default_value=value, + size=1, + meta=meta, + transformer=TransformerConstant(value=value), + vector_dist=ConstantVectorDistribution(value=CONSTANT_VECTOR_VALUE), + neighborhood=_empty_neighborhood, + neighborhood_size=0, ) - def __copy__(self): - return Constant(self.name, self.value, meta=self.meta) - - def __hash__(self): - return hash((self.name, self.value)) - - def is_legal(self, value: str | int | float) -> bool: - return value == self.value - - def is_legal_vector(self, value) -> int: - return value == self.value_vector - - def _sample(self, rs: None, size: int | None = None) -> int | np.ndarray: - return 0 if size == 1 else np.zeros((size,)) - - def _transform( - self, vector: np.ndarray | float | int | None, - ) -> np.ndarray | float | int | None: - return self.value - - def _transform_vector( - self, vector: np.ndarray | None, - ) -> np.ndarray | float | int | None: - return self.value - - def _transform_scalar( - self, vector: float | int | None, - ) -> np.ndarray | float | int | None: - return self.value - - def _inverse_transform( - self, - vector: np.ndarray | float | int, - ) -> np.ndarray | int | float: - if vector != self.value: - return np.NaN - return 0 - - def has_neighbors(self) -> bool: - return False - - def get_num_neighbors(self, value=None) -> int: - return 0 - - def get_neighbors( - self, value: Any, rs: np.random.RandomState, number: int, transform: bool = False, - ) -> list: - return [] - - def pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the parameter in - the original parameter space (the one specified by the user). - For each hyperparameter type, there is also a method _pdf which - operates on the transformed (and possibly normalized) parameter - space. Only legal values return a positive probability density, - otherwise zero. - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - if vector.ndim != 1: - raise ValueError("Method pdf expects a one-dimensional numpy array") - return self._pdf(vector) - - def _pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal - distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - return (vector == self.value).astype(float) - - def get_max_density(self): - return 1.0 - - def get_size(self) -> float: - return 1.0 - class UnParametrizedHyperparameter(Constant): pass diff --git a/ConfigSpace/hyperparameters/float_hyperparameter.pxd b/ConfigSpace/hyperparameters/float_hyperparameter.pxd deleted file mode 100644 index 66d39b1e..00000000 --- a/ConfigSpace/hyperparameters/float_hyperparameter.pxd +++ /dev/null @@ -1,19 +0,0 @@ -import numpy as np -cimport numpy as np -np.import_array() - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t - -from .numerical cimport NumericalHyperparameter - - -cdef class FloatHyperparameter(NumericalHyperparameter): - cpdef double _transform_scalar(self, double scalar) - cpdef np.ndarray _transform_vector(self, np.ndarray vector) diff --git a/ConfigSpace/hyperparameters/float_hyperparameter.py b/ConfigSpace/hyperparameters/float_hyperparameter.py index 1f3ba662..5111abc9 100644 --- a/ConfigSpace/hyperparameters/float_hyperparameter.py +++ b/ConfigSpace/hyperparameters/float_hyperparameter.py @@ -1,94 +1,16 @@ from __future__ import annotations -import numpy as np - -from ConfigSpace.hyperparameters.numerical import NumericalHyperparameter - - -class FloatHyperparameter(NumericalHyperparameter): - def __init__( - self, - name: str, - default_value: int | float, - meta: dict | None = None, - ) -> None: - super().__init__(name, default_value, meta) - - def is_legal(self, value: int | float) -> bool: - raise NotImplementedError() - - def is_legal_vector(self, value) -> int: - raise NotImplementedError() - - def check_default(self, default_value: int | float) -> float: - raise NotImplementedError() +from dataclasses import dataclass - def _transform( - self, - vector: np.ndarray | float | int, - ) -> np.ndarray | float | int | None: - try: - if isinstance(vector, np.ndarray): - return self._transform_vector(vector) - return self._transform_scalar(vector) - except ValueError: - return None - - def _transform_scalar(self, scalar: float) -> float: - raise NotImplementedError() - - def _transform_vector(self, vector: np.ndarray) -> np.ndarray: - raise NotImplementedError() - - def pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the parameter in - the original parameter space (the one specified by the user). - For each parameter type, there is also a method _pdf which - operates on the transformed (and possibly normalized) parameter - space. Only legal values return a positive probability density, - otherwise zero. - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - if vector.ndim != 1: - raise ValueError("Method pdf expects a one-dimensional numpy array") - vector = self._inverse_transform(vector) - return self._pdf(vector) - - def _pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal - distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). +import numpy as np - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. +from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter +from ConfigSpace.hyperparameters.numerical_hyperparameter import NumericalHyperparameter - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - raise NotImplementedError() - def get_max_density(self) -> float: - """ - Returns the maximal density on the pdf for the parameter (so not - the mode, but the value of the pdf on the mode). - """ - raise NotImplementedError() +@dataclass(init=False) +class FloatHyperparameter( + Hyperparameter[np.float64, np.float64], + NumericalHyperparameter[np.float64], +): + pass diff --git a/ConfigSpace/hyperparameters/hyperparameter.pxd b/ConfigSpace/hyperparameters/hyperparameter.pxd deleted file mode 100644 index c51e0016..00000000 --- a/ConfigSpace/hyperparameters/hyperparameter.pxd +++ /dev/null @@ -1,22 +0,0 @@ -import numpy as np -cimport numpy as np -np.import_array() - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t - - -cdef class Hyperparameter(object): - cdef public str name - cdef public default_value - cdef public DTYPE_t normalized_default_value - cdef public dict meta - - cpdef int compare_vector(self, DTYPE_t value, DTYPE_t value2) - cpdef bint is_legal_vector(self, DTYPE_t value) diff --git a/ConfigSpace/hyperparameters/hyperparameter.py b/ConfigSpace/hyperparameters/hyperparameter.py index ed884c7f..87ab107c 100644 --- a/ConfigSpace/hyperparameters/hyperparameter.py +++ b/ConfigSpace/hyperparameters/hyperparameter.py @@ -1,174 +1,331 @@ from __future__ import annotations -from enum import Enum, auto +import warnings +from collections.abc import Callable, Hashable, Mapping, Sequence +from dataclasses import dataclass, field, replace +from enum import Enum +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Generic, + Protocol, + TypeVar, + overload, + runtime_checkable, +) +from typing_extensions import Self import numpy as np +import numpy.typing as npt +from scipy.stats._distn_infrastructure import rv_continuous_frozen, rv_discrete_frozen +from ConfigSpace.hyperparameters._hp_components import ( + DType, + VDType, + _Neighborhood, + _Transformer, +) -class Comparison(Enum): - """Enumeration of possible comparison results.""" +if TYPE_CHECKING: + from ConfigSpace.hyperparameters._distributions import VectorDistribution - LESS_THAN = auto() - EQUAL = auto() - GREATER_THAN = auto() - NOT_EQUAL = auto() +class Comparison(str, Enum): + LESS_THAN = "less" + GREATER_THAN = "greater" + EQUAL = "equal" + UNEQUAL = "unequal" -class Hyperparameter: - def __init__(self, name: str, meta: dict | None) -> None: - if not isinstance(name, str): - raise TypeError( - "The name of a hyperparameter must be an instance of" - f" {str!s}, but is {type(name)}.", + +@dataclass(init=False) +class Hyperparameter(Generic[DType, VDType]): + orderable: ClassVar[bool] = False + + name: str = field(hash=True) + default_value: DType = field(hash=True) + vector_dist: VectorDistribution[VDType] = field(hash=True) + meta: Mapping[Hashable, Any] | None = field(hash=True) + + size: int | float = field(hash=True, repr=False) + + normalized_default_value: VDType = field(hash=True, init=False, repr=False) + + _legal_vector: Callable[[VDType], bool] = field(hash=True) + _transformer: _Transformer[DType, VDType] = field(hash=True) + _neighborhood: _Neighborhood[VDType] = field(hash=True) + _neighborhood_size: Callable[[DType | None], int | float] | float | int = field( + repr=False, + ) + + def __init__( + self, + name: str, + *, + default_value: DType, + size: int | float, + vector_dist: VectorDistribution[VDType], + transformer: _Transformer[DType, VDType], + neighborhood: _Neighborhood[VDType], + neighborhood_size: Callable[[DType | None], int] | int | float = np.inf, + meta: Mapping[Hashable, Any] | None = None, + ): + self.name = name + self.default_value = default_value + self.vector_dist = vector_dist + self.meta = meta if meta is not None else {} + + self.size = size + + self._transformer = transformer + self._neighborhood = neighborhood + self._neighborhood_size = neighborhood_size + + if not self.legal_value(self.default_value): + raise ValueError( + f"Default value {self.default_value} is not within the legal range.", ) - self.name: str = name - self.meta = meta - def __repr__(self): - raise NotImplementedError() + self.normalized_default_value = self.to_vector(default_value) - def is_legal(self, value): - raise NotImplementedError() + @property + def lower_vectorized(self) -> VDType: + return self.vector_dist.lower - def is_legal_vector(self, value) -> int: - """ - Check whether the given value is a legal value for the vector - representation of this hyperparameter. + @property + def upper_vectorized(self) -> VDType: + return self.vector_dist.upper - Parameters - ---------- - value - the vector value to check + @overload + def sample_value( + self, + size: None = None, + *, + seed: np.random.RandomState | None = None, + ) -> DType: ... - Returns - ------- - bool - True if the given value is a legal vector value, otherwise False + @overload + def sample_value( + self, + size: int, + *, + seed: np.random.RandomState | None = None, + ) -> npt.NDArray[DType]: ... - """ - raise NotImplementedError() + def sample_value( + self, + size: int | None = None, + *, + seed: np.random.RandomState | None = None, + ) -> DType | npt.NDArray[DType]: + """Sample a value from this hyperparameter.""" + samples = self.sample_vector(size=size, seed=seed) + return self.to_value(samples) + + @overload + def sample_vector( + self, + size: None = None, + *, + seed: np.random.RandomState | None = None, + ) -> VDType: ... - def sample(self, rs): - vector = self._sample(rs) - return self._transform(vector) + @overload + def sample_vector( + self, + size: int, + *, + seed: np.random.RandomState | None = None, + ) -> npt.NDArray[VDType]: ... - def rvs( + def sample_vector( self, size: int | None = None, - random_state: int | np.random.RandomState | None = None, - ) -> float | np.ndarray: - """ - scipy compatibility wrapper for ``_sample``, - allowing the hyperparameter to be used in sklearn API - hyperparameter searchers, eg. GridSearchCV. - - """ - - # copy-pasted from scikit-learn utils/validation.py - def check_random_state(seed): - """ - Turn seed into a np.random.RandomState instance - If seed is None (or np.random), return the RandomState singleton used - by np.random. - If seed is an int, return a new RandomState instance seeded with seed. - If seed is already a RandomState instance, return it. - If seed is a new-style np.random.Generator, return it. - Otherwise, raise ValueError. - - """ - if seed is None or seed is np.random: - return np.random.mtrand._rand - if isinstance(seed, (int, np.integer)): - return np.random.RandomState(seed) - if isinstance(seed, np.random.RandomState): - return seed - try: - # Generator is only available in numpy >= 1.17 - if isinstance(seed, np.random.Generator): - return seed - except AttributeError: - pass - raise ValueError( - "%r cannot be used to seed a numpy.random.RandomState" " instance" % seed, + *, + seed: np.random.RandomState | None = None, + ) -> VDType | npt.NDArray[VDType]: + if size is None: + return self.vector_dist.sample(n=1, seed=seed)[0] + return self.vector_dist.sample(n=size, seed=seed) + + def legal_vector(self, vector: VDType) -> bool: + return self.vector_dist.in_support(vector) + + def legal_value(self, value: DType) -> bool: + vector = self.to_vector(value) + return self.legal_vector(vector) + + def rvs(self, random_state: np.random.RandomState) -> DType: + vector = self.sample_vector(seed=random_state) + return self.to_value(vector) + + @overload + def to_value(self, vector: VDType) -> DType: ... + + @overload + def to_value(self, vector: npt.NDArray[VDType]) -> npt.NDArray[DType]: ... + + def to_value( + self, + vector: VDType | npt.NDArray[VDType], + ) -> DType | npt.NDArray[DType]: + match vector: + case np.ndarray(): + return self._transformer.to_value(vector) + case _: + return self._transformer.to_value(np.array([vector]))[0] + + @overload + def to_vector(self, value: DType) -> VDType: ... + + @overload + def to_vector( + self, + value: Sequence[DType] | npt.NDArray, + ) -> npt.NDArray[VDType]: ... + + def to_vector( + self, + value: DType | Sequence[DType] | npt.NDArray, + ) -> VDType | npt.NDArray[VDType]: + match value: + case np.ndarray(): + return self._transformer.to_vector(value) + case Sequence(): + return self._transformer.to_vector(value) + case _: + return self._transformer.to_vector(np.array([value]))[0] + + def neighbors_vectorized( + self, + vector: VDType, + n: int, + *, + std: float | None = None, + seed: np.random.RandomState | None = None, + ) -> npt.NDArray[VDType]: + if std is not None: + assert 0.0 <= std <= 1.0, f"std must be in [0, 1], got {std}" + + return self._neighborhood(vector, n, std=std, seed=seed) + + def neighbors_values( + self, + value: DType, + n: int, + *, + std: float, + seed: np.random.RandomState | None = None, + ) -> npt.NDArray[DType]: + vector = self.to_vector(value) + return self.to_value( + vector=self.neighbors_vectorized(vector, n, std=std, seed=seed), + ) + + def get_neighbors( + self, + value: VDType, + rs: np.random.RandomState, + number: int | None = None, + std: float | None = None, + transform: bool = False, + ) -> npt.NDArray: + warnings.warn( + "Please use" + "`neighbors_vectorized(value=value, n=number, seed=rs, std=str)`" + " instead. This is deprecated and will be removed in the future." + " If you need `transform=True`, please apply `to_value` to the result.", + DeprecationWarning, + stacklevel=2, + ) + if number is None: + warnings.warn( + "Please provide a number of neighbors to sample. The" + " default used to be `4` but will be explicitly required" + " in the futurefuture.", + DeprecationWarning, + stacklevel=2, ) + number = 4 + + neighbors = self.neighbors_vectorized(value, number, std=std, seed=rs) + if transform: + return self.to_value(neighbors) + return neighbors + + def vector_pdf(self, vector: npt.NDArray[VDType]) -> npt.NDArray[np.float64]: + match self.vector_dist: + case rv_continuous_frozen(): + return self.vector_dist.pdf(vector) + case rv_discrete_frozen(): + return self.vector_dist.pmf(vector) + case _: + raise NotImplementedError( + "Only continuous and discrete distributions are supported." + f"Got {self.vector_dist}", + ) + + def value_pdf( + self, + values: Sequence[DType] | npt.NDArray, + ) -> npt.NDArray[np.float64]: + vector = self.to_vector(values) + return self.vector_pdf(vector) + + def copy(self, **kwargs: Any) -> Self: + return replace(self, **kwargs) + + def get_size(self) -> int | float: + warnings.warn( + "Please just use the `.size` attribute directly", + DeprecationWarning, + stacklevel=2, + ) + return self.size - # if size=None, return a value, but if size=1, return a 1-element array + def compare_value( + self, + value: DType, + other_value: DType, + ) -> Comparison: + vector = self.to_vector(value) + other_vector = self.to_vector(other_value) + return self.compare_vector(vector, other_vector) + + def compare_vector( + self, + vector: VDType, + other_vector: VDType, + ) -> Comparison: + if vector == other_vector: + return Comparison.EQUAL + + if not self.orderable: + return Comparison.UNEQUAL + + if vector < other_vector: + return Comparison.LESS_THAN + return Comparison.GREATER_THAN + + def get_num_neighbors(self, value: DType | None = None) -> int | float: + return ( + self._neighborhood_size(value) + if callable(self._neighborhood_size) + else self._neighborhood_size + ) - vector = self._sample( - rs=check_random_state(random_state), - size=size if size is not None else 1, + def has_neighbors(self) -> bool: + warnings.warn( + "Please use `get_num_neighbors() > 0` instead.", + DeprecationWarning, + stacklevel=2, ) - if size is None: - vector = vector[0] - - return self._transform(vector) - - def _sample(self, rs, size): - raise NotImplementedError() - - def _transform( - self, - vector: np.ndarray | float | int, - ) -> np.ndarray | float | int | None: - raise NotImplementedError() - - def _inverse_transform(self, vector): - raise NotImplementedError() - - def has_neighbors(self): - raise NotImplementedError() - - def get_neighbors(self, value, rs, number, transform=False): - raise NotImplementedError() - - def get_num_neighbors(self, value): - raise NotImplementedError() - - def compare_vector(self, value: float, value2: float) -> Comparison: - raise NotImplementedError() - - def pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the hyperparameter in - the hyperparameter space (the one specified by the user). - For each hyperparameter type, there is also a method _pdf which - operates on the transformed (and possibly normalized) hyperparameter - space. Only legal values return a positive probability density, - otherwise zero. - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - raise NotImplementedError() - - def _pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the hyperparameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal - distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - raise NotImplementedError() - - def get_size(self) -> float: - raise NotImplementedError() + return self.get_num_neighbors() > 0 + + +HPType = TypeVar("HPType", bound=Hyperparameter) + + +@runtime_checkable +class HyperparameterWithPrior(Protocol[HPType]): + def to_uniform(self) -> HPType: ... diff --git a/ConfigSpace/hyperparameters/integer_hyperparameter.pxd b/ConfigSpace/hyperparameters/integer_hyperparameter.pxd deleted file mode 100644 index 6e90dae9..00000000 --- a/ConfigSpace/hyperparameters/integer_hyperparameter.pxd +++ /dev/null @@ -1,20 +0,0 @@ -import numpy as np -cimport numpy as np -np.import_array() - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t - -from .numerical cimport NumericalHyperparameter - - -cdef class IntegerHyperparameter(NumericalHyperparameter): - cdef ufhp - cpdef long long _transform_scalar(self, double scalar) - cpdef np.ndarray _transform_vector(self, np.ndarray vector) diff --git a/ConfigSpace/hyperparameters/integer_hyperparameter.py b/ConfigSpace/hyperparameters/integer_hyperparameter.py index c594c4c1..a998fecf 100644 --- a/ConfigSpace/hyperparameters/integer_hyperparameter.py +++ b/ConfigSpace/hyperparameters/integer_hyperparameter.py @@ -1,101 +1,19 @@ from __future__ import annotations -import numpy as np - -from ConfigSpace.hyperparameters.numerical import NumericalHyperparameter - - -class IntegerHyperparameter(NumericalHyperparameter): - def __init__(self, name: str, default_value: int, meta: dict | None = None) -> None: - super().__init__(name, default_value, meta) - - def is_legal(self, value: int) -> bool: - raise NotImplementedError - - def is_legal_vector(self, value) -> int: - raise NotImplementedError - - def check_default(self, default_value) -> int: - raise NotImplementedError - - def check_int(self, parameter: int, name: str) -> int: - if abs(int(parameter) - parameter) > 0.00000001 and type(parameter) is not int: - raise ValueError( - f"For the Integer parameter {name}, the value must be " - f"an Integer, too. Right now it is a {type(parameter)} with value" - f" {parameter!s}.", - ) - return int(parameter) +from dataclasses import dataclass - def _transform( - self, - vector: np.ndarray | float | int, - ) -> np.ndarray | float | int | None: - try: - if isinstance(vector, np.ndarray): - return self._transform_vector(vector) - return self._transform_scalar(vector) - except ValueError: - return None - - def _transform_scalar(self, scalar: float) -> float: - raise NotImplementedError() - - def _transform_vector(self, vector: np.ndarray) -> np.ndarray: - raise NotImplementedError() - - def pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the hyperparameter in - the hyperparameter space (the one specified by the user). - For each hyperparameter type, there is also a method _pdf which - operates on the transformed (and possibly normalized) hyperparameter - space. Only legal values return a positive probability density, - otherwise zero. - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - if vector.ndim != 1: - raise ValueError("Method pdf expects a one-dimensional numpy array") - is_integer = (np.round(vector) == vector).astype(int) - vector = self._inverse_transform(vector) - return self._pdf(vector) * is_integer - - def _pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal - distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). Optimally, an IntegerHyperparameter - should have a corresponding float, which can be utlized for the calls - to the probability density function (see e.g. NormalIntegerHyperparameter). +import numpy as np - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. +from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter +from ConfigSpace.hyperparameters.numerical_hyperparameter import NumericalHyperparameter - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - raise NotImplementedError() - def get_max_density(self) -> float: - """ - Returns the maximal density on the pdf for the parameter (so not - the mode, but the value of the pdf on the mode). - """ - raise NotImplementedError() +@dataclass(init=False) +class IntegerHyperparameter( + Hyperparameter[np.int64, np.float64], + NumericalHyperparameter[np.int64], +): + def _neighborhood_size(self, value: np.int64 | None) -> int: + if value is None or self.lower <= value <= self.upper: + return int(self.size) + return int(self.size) - 1 diff --git a/ConfigSpace/hyperparameters/normal_float.pxd b/ConfigSpace/hyperparameters/normal_float.pxd deleted file mode 100644 index b6f5d1eb..00000000 --- a/ConfigSpace/hyperparameters/normal_float.pxd +++ /dev/null @@ -1,19 +0,0 @@ -import numpy as np -cimport numpy as np -np.import_array() - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t - -from .float_hyperparameter cimport FloatHyperparameter - - -cdef class NormalFloatHyperparameter(FloatHyperparameter): - cdef public mu - cdef public sigma diff --git a/ConfigSpace/hyperparameters/normal_float.py b/ConfigSpace/hyperparameters/normal_float.py index 6d958e78..b4bfe131 100644 --- a/ConfigSpace/hyperparameters/normal_float.py +++ b/ConfigSpace/hyperparameters/normal_float.py @@ -1,374 +1,117 @@ from __future__ import annotations -import io -import math -from typing import TYPE_CHECKING, Any +from collections.abc import Hashable, Mapping +from dataclasses import dataclass +from typing import Any, ClassVar import numpy as np -from scipy.stats import norm, truncnorm - +from scipy.stats import truncnorm +from scipy.stats._distn_infrastructure import rv_continuous_frozen + +from ConfigSpace.hyperparameters._distributions import ScipyContinuousDistribution +from ConfigSpace.hyperparameters._hp_components import ( + ROUND_PLACES, + VECTORIZED_NUMERIC_LOWER, + VECTORIZED_NUMERIC_UPPER, + UnitScaler, +) from ConfigSpace.hyperparameters.float_hyperparameter import FloatHyperparameter +from ConfigSpace.hyperparameters.hyperparameter import HyperparameterWithPrior +from ConfigSpace.hyperparameters.normal_integer import NormalIntegerHyperparameter from ConfigSpace.hyperparameters.uniform_float import UniformFloatHyperparameter -if TYPE_CHECKING: - from ConfigSpace.hyperparameters.normal_integer import NormalIntegerHyperparameter +@dataclass(init=False) +class NormalFloatHyperparameter( + FloatHyperparameter, + HyperparameterWithPrior[UniformFloatHyperparameter], +): + orderable: ClassVar[bool] = True + mu: float + sigma: float -class NormalFloatHyperparameter(FloatHyperparameter): def __init__( self, name: str, mu: int | float, sigma: int | float, + lower: float | int, + upper: float | int, default_value: None | float = None, - q: int | float | None = None, log: bool = False, - lower: float | int | None = None, - upper: float | int | None = None, - meta: dict | None = None, + meta: Mapping[Hashable, Any] | None = None, ) -> None: - r""" - A normally distributed float hyperparameter. - - Its values are sampled from a normal distribution - :math:`\mathcal{N}(\mu, \sigma^2)`. - - >>> from ConfigSpace import NormalFloatHyperparameter - >>> - >>> NormalFloatHyperparameter('n', mu=0, sigma=1, log=False) - n, Type: NormalFloat, Mu: 0.0 Sigma: 1.0, Default: 0.0 + if mu < lower or mu > upper: + raise ValueError( + f"mu={mu} must be in the range [{lower}, {upper}] for hyperparameter" + f"'{name}'", + ) - Parameters - ---------- - name : str - Name of the hyperparameter, with which it can be accessed - mu : int, float - Mean of the distribution - sigma : int, float - Standard deviation of the distribution - default_value : int, float, optional - Sets the default value of a hyperparameter to a given value - q : int, float, optional - Quantization factor - log : bool, optional - If ``True``, the values of the hyperparameter will be sampled - on a logarithmic scale. Default to ``False`` - lower : int, float, optional - Lower bound of a range of values from which the hyperparameter will be sampled - upper : int, float, optional - Upper bound of a range of values from which the hyperparameter will be sampled - meta : Dict, optional - Field for holding meta data provided by the user. - Not used by the configuration space. - """ + self.lower = np.float64(lower) + self.upper = np.float64(upper) self.mu = float(mu) self.sigma = float(sigma) - self.q = float(q) if q is not None else None self.log = bool(log) - self.lower: float | None = None - self.upper: float | None = None - - if (lower is not None) ^ (upper is not None): - raise ValueError( - "Only one bound was provided when both lower and upper bounds must be provided.", - ) - - if lower is not None and upper is not None: - self.lower = float(lower) - self.upper = float(upper) - if self.lower >= self.upper: - raise ValueError( - f"Upper bound {self.upper:f} must be larger than lower bound " - f"{self.lower:f} for hyperparameter {name}", - ) - if log and self.lower <= 0: - raise ValueError( - f"Negative lower bound ({self.lower:f}) for log-scale " - f"hyperparameter {name} is forbidden.", - ) + try: + scaler = UnitScaler(self.lower, self.upper, log=log) + except ValueError as e: + raise ValueError(f"Hyperparameter '{name}' has illegal settings") from e - self.default_value = self.check_default(default_value) - - if self.log: - if self.q is not None: - lower = self.lower - (np.float64(self.q) / 2.0 - 0.0001) - upper = self.upper + (np.float64(self.q) / 2.0 - 0.0001) - else: - lower = self.lower - upper = self.upper - self._lower = np.log(lower) - self._upper = np.log(upper) - else: - if self.q is not None: - self._lower = self.lower - (self.q / 2.0 - 0.0001) - self._upper = self.upper + (self.q / 2.0 - 0.0001) - else: - self._lower = self.lower - self._upper = self.upper - - if self.q is not None: - # There can be weird rounding errors, so we compare the result against self.q, see - # In [13]: 2.4 % 0.2 - # Out[13]: 0.1999999999999998 - if np.round((self.upper - self.lower) % self.q, 10) not in (0, self.q): - raise ValueError( - f"Upper bound ({self.upper:f}) - lower bound ({self.lower:f}) must be a multiple of q ({self.q:f})", - ) - - default_value = self.check_default(default_value) - super().__init__(name, default_value, meta) - self.normalized_default_value = self._inverse_transform(self.default_value) - - def __repr__(self) -> str: - repr_str = io.StringIO() - - if self.lower is None or self.upper is None: - repr_str.write( - f"{self.name}, Type: NormalFloat," - f" Mu: {self.mu!r}" - f" Sigma: {self.sigma!r}," - f" Default: {self.default_value!r}", - ) + if default_value is None: + _default_value = np.float64(self.mu) else: - repr_str.write( - "{}, Type: NormalFloat, Mu: {} Sigma: {}, Range: [{}, {}], Default: {}".format( - self.name, - repr(self.mu), - repr(self.sigma), - repr(self.lower), - repr(self.upper), - repr(self.default_value), - ), - ) - - if self.log: - repr_str.write(", on log-scale") - if self.q is not None: - repr_str.write(", Q: %s" % str(self.q)) - repr_str.seek(0) - return repr_str.getvalue() + _default_value = np.float64(round(default_value, ROUND_PLACES)) - def __eq__(self, other: Any) -> bool: - """ - This method implements a comparison between self and another - object. - - Additionally, it defines the __ne__() as stated in the - documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result - unless it is NotImplemented. + truncnorm_dist = truncnorm( # type: ignore + a=(self.lower - self.mu) / self.sigma, + b=(self.upper - self.mu) / self.sigma, + loc=self.mu, + scale=self.sigma, + ) + assert isinstance(truncnorm_dist, rv_continuous_frozen) - """ - if not isinstance(other, self.__class__): - return False + max_density_point = np.clip( + scaler.to_vector(np.array([self.mu]))[0], + a_min=VECTORIZED_NUMERIC_LOWER, + a_max=VECTORIZED_NUMERIC_UPPER, + ) + max_density_value: float = truncnorm_dist.pdf(max_density_point) + assert isinstance(max_density_value, float) - return ( - self.name == other.name - and self.default_value == other.default_value - and self.mu == other.mu - and self.sigma == other.sigma - and self.log == other.log - and self.q == other.q - and self.lower == other.lower - and self.upper == other.upper + vect_dist = ScipyContinuousDistribution( + rv=truncnorm_dist, + dtype=np.float64, + max_density_value=max_density_value, + ) + super().__init__( + name=name, + size=np.inf, + default_value=_default_value, + meta=meta, + transformer=scaler, + vector_dist=vect_dist, + neighborhood=vect_dist.neighborhood, + neighborhood_size=np.inf, ) - def __copy__(self): - return NormalFloatHyperparameter( + def to_uniform(self) -> UniformFloatHyperparameter: + return UniformFloatHyperparameter( name=self.name, - default_value=self.default_value, - mu=self.mu, - sigma=self.sigma, - log=self.log, - q=self.q, lower=self.lower, upper=self.upper, - meta=self.meta, - ) - - def __hash__(self): - return hash((self.name, self.mu, self.sigma, self.log, self.q, self.lower, self.upper)) - - def to_uniform(self, z: int = 3) -> UniformFloatHyperparameter: - if self.lower is None or self.upper is None: - lb = self.mu - (z * self.sigma) - ub = self.mu + (z * self.sigma) - else: - lb = self.lower - ub = self.upper - - return UniformFloatHyperparameter( - self.name, - lb, - ub, default_value=self.default_value, - q=self.q, log=self.log, meta=self.meta, ) - def check_default(self, default_value: int | float | None) -> int | float: - if default_value is None: - if self.log: - return self._transform_scalar(self.mu) - else: - return self.mu - - elif self.is_legal(default_value): - return default_value - else: - raise ValueError("Illegal default value %s" % str(default_value)) - def to_integer(self) -> NormalIntegerHyperparameter: - q_int = None if self.q is None else int(np.rint(self.q)) - if self.lower is None: - lower = None - upper = None - else: - lower = np.ceil(self.lower) - upper = np.floor(self.upper) - - from ConfigSpace.hyperparameters.normal_integer import NormalIntegerHyperparameter - return NormalIntegerHyperparameter( - self.name, - int(np.rint(self.mu)), - self.sigma, - lower=lower, - upper=upper, - default_value=int(np.rint(self.default_value)), - q=q_int, + name=self.name, + mu=round(self.mu), + sigma=self.sigma, + lower=np.ceil(self.lower), + upper=np.floor(self.upper), + default_value=round(self.default_value), log=self.log, ) - - def is_legal(self, value: float) -> bool: - return ( - (isinstance(value, (float, int, np.number))) - and (self.lower is None or value >= self.lower) - and (self.upper is None or value <= self.upper) - ) - - def is_legal_vector(self, value) -> int: - return isinstance(value, (float, int)) - - def _sample( - self, - rs: np.random.RandomState, - size: int | None = None, - ) -> np.ndarray | float: - if self.lower is None: - mu = self.mu - sigma = self.sigma - return rs.normal(mu, sigma, size=size) - else: - mu = self.mu - sigma = self.sigma - lower = self._lower - upper = self._upper - a = (lower - mu) / sigma - b = (upper - mu) / sigma - - return truncnorm.rvs(a, b, loc=mu, scale=sigma, size=size, random_state=rs) - - def _transform_vector(self, vector: np.ndarray) -> np.ndarray: - if np.isnan(vector).any(): - raise ValueError("Vector %s contains NaN's" % vector) - if self.log: - vector = np.exp(vector) - if self.q is not None: - vector = np.rint(vector / self.q) * self.q - return vector - - def _transform_scalar(self, scalar: float) -> float: - if scalar != scalar: - raise ValueError("Number %s is NaN" % scalar) - if self.log: - scalar = math.exp(scalar) - if self.q is not None: - scalar = np.round(scalar / self.q) * self.q - return scalar - - def _inverse_transform( - self, - vector: float | np.ndarray | None, - ) -> float | np.ndarray: - # TODO: Should probably use generics here - if vector is None: - return np.NaN - - if self.log: - vector = np.log(vector) - - return vector - - def get_neighbors( - self, - value: float, - rs: np.random.RandomState, - number: int = 4, - transform: bool = False, - ) -> list[float]: - neighbors = [] - for _i in range(number): - new_value = rs.normal(value, self.sigma) - - if self.lower is not None and self.upper is not None: - new_value = min(max(new_value, self.lower), self.upper) - - neighbors.append(new_value) - return neighbors - - def get_size(self) -> float: - if self.q is None: - return np.inf - elif self.lower is None: - return np.inf - else: - return np.rint((self.upper - self.lower) / self.q) + 1 - - def _pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal - distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - mu = self.mu - sigma = self.sigma - if self.lower is None: - return norm(loc=mu, scale=sigma).pdf(vector) - else: - mu = self.mu - sigma = self.sigma - lower = self._lower - upper = self._upper - a = (lower - mu) / sigma - b = (upper - mu) / sigma - - return truncnorm(a, b, loc=mu, scale=sigma).pdf(vector) - - def get_max_density(self) -> float: - if self.lower is None: - return self._pdf(np.array([self.mu]))[0] - - if self.mu < self._lower: - return self._pdf(np.array([self._lower]))[0] - elif self.mu > self._upper: - return self._pdf(np.array([self._upper]))[0] - else: - return self._pdf(np.array([self.mu]))[0] diff --git a/ConfigSpace/hyperparameters/normal_integer.pxd b/ConfigSpace/hyperparameters/normal_integer.pxd deleted file mode 100644 index d5e8e1a7..00000000 --- a/ConfigSpace/hyperparameters/normal_integer.pxd +++ /dev/null @@ -1,21 +0,0 @@ -import numpy as np -cimport numpy as np -np.import_array() - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t - -from .integer_hyperparameter cimport IntegerHyperparameter - - -cdef class NormalIntegerHyperparameter(IntegerHyperparameter): - cdef public mu - cdef public sigma - cdef public nfhp - cdef public normalization_constant diff --git a/ConfigSpace/hyperparameters/normal_integer.py b/ConfigSpace/hyperparameters/normal_integer.py index 4e8c3805..35cbc025 100644 --- a/ConfigSpace/hyperparameters/normal_integer.py +++ b/ConfigSpace/hyperparameters/normal_integer.py @@ -1,401 +1,106 @@ from __future__ import annotations -import io -import warnings -from itertools import count -from typing import Any +from collections.abc import Hashable, Mapping +from dataclasses import dataclass +from typing import Any, ClassVar import numpy as np -from more_itertools import roundrobin -from scipy.stats import norm, truncnorm - -from ConfigSpace.functional import arange_chunked, center_range +from scipy.stats import truncnorm + +from ConfigSpace.hyperparameters._distributions import ( + DiscretizedContinuousScipyDistribution, +) +from ConfigSpace.hyperparameters._hp_components import ( + VECTORIZED_NUMERIC_LOWER, + VECTORIZED_NUMERIC_UPPER, + UnitScaler, +) +from ConfigSpace.hyperparameters.hyperparameter import HyperparameterWithPrior from ConfigSpace.hyperparameters.integer_hyperparameter import IntegerHyperparameter -from ConfigSpace.hyperparameters.normal_float import NormalFloatHyperparameter from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter -# OPTIM: Some operations generate an arange which could blowup memory if -# done over the entire space of integers (int32/64). -# To combat this, `arange_chunked` is used in scenarios where reducion -# operations over all the elments could be done in partial steps independantly. -# For example, a sum over the pdf values could be done in chunks. -# This may add some small overhead for smaller ranges but is unlikely to -# be noticable. -ARANGE_CHUNKSIZE = 10_000_000 +@dataclass(init=False) +class NormalIntegerHyperparameter( + IntegerHyperparameter, + HyperparameterWithPrior[UniformIntegerHyperparameter], +): + orderable: ClassVar[bool] = True + mu: float + sigma: float -class NormalIntegerHyperparameter(IntegerHyperparameter): def __init__( self, name: str, - mu: int, - sigma: int | float, - default_value: int | None = None, - q: None | int = None, + mu: float, + sigma: float, + lower: int, + upper: int, + default_value: int | np.integer | None = None, log: bool = False, - lower: int | None = None, - upper: int | None = None, - meta: dict | None = None, + meta: Mapping[Hashable, Any] | None = None, ) -> None: - r""" - A normally distributed integer hyperparameter. - - Its values are sampled from a normal distribution - :math:`\mathcal{N}(\mu, \sigma^2)`. - - >>> from ConfigSpace import NormalIntegerHyperparameter - >>> - >>> NormalIntegerHyperparameter(name='n', mu=0, sigma=1, log=False) - n, Type: NormalInteger, Mu: 0 Sigma: 1, Default: 0 - - Parameters - ---------- - name : str - Name of the hyperparameter with which it can be accessed - mu : int - Mean of the distribution, from which hyperparameter is sampled - sigma : int, float - Standard deviation of the distribution, from which - hyperparameter is sampled - default_value : int, optional - Sets the default value of a hyperparameter to a given value - q : int, optional - Quantization factor - log : bool, optional - If ``True``, the values of the hyperparameter will be sampled - on a logarithmic scale. Defaults to ``False`` - lower : int, float, optional - Lower bound of a range of values from which the hyperparameter will be sampled - upper : int, float, optional - Upper bound of a range of values from which the hyperparameter will be sampled - meta : Dict, optional - Field for holding meta data provided by the user. - Not used by the configuration space. - - """ - super().__init__(name, default_value, meta) - - self.mu = mu - self.sigma = sigma - - if default_value is not None: - default_value = self.check_int(default_value, self.name) - - if q is not None: - if q < 1: - warnings.warn( - "Setting quantization < 1 for Integer " - "Hyperparameter '%s' has no effect." % name, - ) - self.q = None - else: - self.q = self.check_int(q, "q") - else: - self.q = None + self.mu = float(mu) + self.sigma = float(sigma) self.log = bool(log) + self.lower = np.int64(lower) + self.upper = np.int64(upper) - if (lower is not None) ^ (upper is not None): - raise ValueError( - "Only one bound was provided when both lower and upper bounds must be provided.", - ) - - self.lower: int | None = None - self.upper: int | None = None - if lower is not None and upper is not None: - self.upper = self.check_int(upper, "upper") - self.lower = self.check_int(lower, "lower") - if self.lower >= self.upper: - raise ValueError( - "Upper bound %d must be larger than lower bound " - "%d for hyperparameter %s" % (self.lower, self.upper, name), - ) - if log and self.lower <= 0: - raise ValueError( - "Negative lower bound (%d) for log-scale " - "hyperparameter %s is forbidden." % (self.lower, name), - ) - - self.nfhp = NormalFloatHyperparameter( - self.name, - self.mu, - self.sigma, - log=self.log, - q=self.q, - lower=self.lower, - upper=self.upper, - default_value=default_value, - ) - - self.default_value = self.check_default(default_value) - self.normalized_default_value = self._inverse_transform(self.default_value) - - if (self.lower is None) or (self.upper is None): - # Since a bound is missing, the pdf cannot be normalized. Working with the unnormalized variant) - self.normalization_constant = 1 - else: - self.normalization_constant = self._compute_normalization() + try: + scaler = UnitScaler(self.lower, self.upper, log=self.log) + except ValueError as e: + raise ValueError(f"Hyperparameter '{name}' has illegal settings") from e - def __repr__(self) -> str: - repr_str = io.StringIO() - - if self.lower is None or self.upper is None: - repr_str.write( - f"{self.name}, Type: NormalInteger, Mu: {self.mu!r} Sigma: {self.sigma!r}, Default: {self.default_value!r}", - ) + if default_value is None: + _default_value = np.rint( + scaler.to_value(np.array([self.mu]))[0], + ).astype(np.int64) else: - repr_str.write( - "{}, Type: NormalInteger, Mu: {} Sigma: {}, Range: [{}, {}], Default: {}".format( - self.name, - repr(self.mu), - repr(self.sigma), - repr(self.lower), - repr(self.upper), - repr(self.default_value), - ), - ) - - if self.log: - repr_str.write(", on log-scale") - if self.q is not None: - repr_str.write(", Q: %s" % str(self.q)) - repr_str.seek(0) - return repr_str.getvalue() - - def __eq__(self, other: Any) -> bool: - """ - This method implements a comparison between self and another - object. - - Additionally, it defines the __ne__() as stated in the - documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result - unless it is NotImplemented. + _default_value = np.rint(default_value).astype(np.int64) - """ - if not isinstance(other, self.__class__): - return False + size = self.upper - self.lower + 1 - return ( - self.name == other.name - and self.mu == other.mu - and self.sigma == other.sigma - and self.log == other.log - and self.q == other.q - and self.lower == other.lower - and self.upper == other.upper - and self.default_value == other.default_value + truncnorm_dist = truncnorm( # type: ignore + a=(self.lower - self.mu) / self.sigma, + b=(self.upper - self.mu) / self.sigma, + loc=self.mu, + scale=self.sigma, ) - - def __hash__(self): - return hash((self.name, self.mu, self.sigma, self.log, self.q, self.lower, self.upper)) - - def __copy__(self): - return NormalIntegerHyperparameter( - name=self.name, - default_value=self.default_value, - mu=self.mu, - sigma=self.sigma, - log=self.log, - q=self.q, - lower=self.lower, - upper=self.upper, - meta=self.meta, + max_density_point = np.clip( + scaler.to_vector(np.array([self.mu]))[0], + a_min=VECTORIZED_NUMERIC_LOWER, + a_max=VECTORIZED_NUMERIC_UPPER, + ) + max_density_value: float = truncnorm_dist.pdf(max_density_point) # type: ignore + assert isinstance(max_density_value, float) + vector_dist = DiscretizedContinuousScipyDistribution( + dist=truncnorm_dist, # type: ignore + steps=size, + max_density_value=max_density_value, + normalization_constant_value=None, # Will compute on demand ) - def to_uniform(self, z: int = 3) -> UniformIntegerHyperparameter: - if self.lower is None or self.upper is None: - lb = np.round(int(self.mu - (z * self.sigma))) - ub = np.round(int(self.mu + (z * self.sigma))) - else: - lb = self.lower - ub = self.upper + # Compute the normalization constant ahead of time + constant = vector_dist._normalization_constant() + vector_dist.normalization_constant_value = constant + + super().__init__( + name=name, + size=int(size), + default_value=_default_value, + meta=meta, + transformer=scaler, + vector_dist=vector_dist, + neighborhood=vector_dist.neighborhood, + neighborhood_size=self._neighborhood_size, + ) + def to_uniform(self) -> UniformIntegerHyperparameter: return UniformIntegerHyperparameter( self.name, - lb, - ub, + lower=self.lower, + upper=self.upper, default_value=self.default_value, - q=self.q, log=self.log, meta=self.meta, ) - - def is_legal(self, value: int) -> bool: - return ( - (isinstance(value, (int, np.integer))) - and (self.lower is None or value >= self.lower) - and (self.upper is None or value <= self.upper) - ) - - def is_legal_vector(self, value) -> int: - return isinstance(value, (float, int)) - - def check_default(self, default_value: int | None) -> int: - if default_value is None: - if self.log: - return self._transform_scalar(self.mu) - else: - return self.mu - - elif self.is_legal(default_value): - return default_value - else: - raise ValueError("Illegal default value %s" % str(default_value)) - - def _sample( - self, - rs: np.random.RandomState, - size: int | None = None, - ) -> np.ndarray | float: - value = self.nfhp._sample(rs, size=size) - # Map all floats which belong to the same integer value to the same - # float value by first transforming it to an integer and then - # transforming it back to a float between zero and one - value = self._transform(value) - return self._inverse_transform(value) - - def _transform_vector(self, vector) -> np.ndarray: - vector = self.nfhp._transform_vector(vector) - return np.rint(vector) - - def _transform_scalar(self, scalar: float) -> float: - scalar = self.nfhp._transform_scalar(scalar) - return int(np.round(scalar)) - - def _inverse_transform( - self, - vector: np.ndarray | float | int, - ) -> np.ndarray | float: - return self.nfhp._inverse_transform(vector) - - def has_neighbors(self) -> bool: - return True - - def get_neighbors( - self, - value: int | float, - rs: np.random.RandomState, - number: int = 4, - transform: bool = False, - ) -> list[int]: - stepsize = self.q if self.q is not None else 1 - bounded = self.lower is not None - mu = self.mu - sigma = self.sigma - - neighbors: set[int] = set() - center = self._transform(value) - - if not bounded: - float_indices = norm.rvs( - loc=mu, - scale=sigma, - size=number, - random_state=rs, - ) - else: - dist = truncnorm( - a=(self.lower - mu) / sigma, - b=(self.upper - mu) / sigma, - loc=center, - scale=sigma, - ) - - float_indices = dist.rvs( - size=number, - random_state=rs, - ) - - possible_neighbors = self._transform_vector(float_indices).astype(np.longlong) - - for possible_neighbor in possible_neighbors: - # If we already happen to have this neighbor, pick the closest - # number around it that is not arelady included - if possible_neighbor in neighbors or possible_neighbor == center: - if bounded: - numbers_around = center_range( - possible_neighbor, self.lower, self.upper, stepsize, - ) - else: - decrement_count = count(possible_neighbor - stepsize, step=-stepsize) - increment_count = count(possible_neighbor + stepsize, step=stepsize) - numbers_around = roundrobin(decrement_count, increment_count) - - valid_numbers_around = ( - n for n in numbers_around if (n not in neighbors and n != center) - ) - possible_neighbor = next(valid_numbers_around, None) - - if possible_neighbor is None: - raise ValueError( - f"Found no more eligble neighbors for value {center}" - f"\nfound {neighbors}", - ) - - # We now have a valid sample, add it to the list of neighbors - neighbors.add(possible_neighbor) - - if transform: - return [self._transform(neighbor) for neighbor in neighbors] - else: - return list(neighbors) - - def _compute_normalization(self): - if self.lower is None: - warnings.warn( - "Cannot normalize the pdf exactly for a NormalIntegerHyperparameter" - f" {self.name} without bounds. Skipping normalization for that hyperparameter.", - ) - return 1 - - else: - if self.upper - self.lower > ARANGE_CHUNKSIZE: - a = (self.lower - self.mu) / self.sigma - b = (self.upper - self.mu) / self.sigma - confidence = 0.999999 - rv = truncnorm(a=a, b=b, loc=self.mu, scale=self.sigma) - u, v = rv.ppf((1 - confidence) / 2), rv.ppf((1 + confidence) / 2) - lb = max(u, self.lower) - ub = min(v, self.upper + 1) - else: - lb = self.lower - ub = self.upper + 1 - - chunks = arange_chunked(lb, ub, chunk_size=ARANGE_CHUNKSIZE) - return sum(self.nfhp.pdf(chunk).sum() for chunk in chunks) - - def _pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal - distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). Optimally, an IntegerHyperparameter - should have a corresponding float, which can be utlized for the calls - to the probability density function (see e.g. NormalIntegerHyperparameter). - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - return self.nfhp._pdf(vector) / self.normalization_constant - - def get_max_density(self): - chunks = arange_chunked(self.lower, self.upper + 1, chunk_size=ARANGE_CHUNKSIZE) - maximum = max(self.nfhp.pdf(chunk).max() for chunk in chunks) - return maximum / self.normalization_constant - - def get_size(self) -> float: - if self.lower is None: - return np.inf - else: - return np.rint((self.upper - self.lower) / self.q) + 1 diff --git a/ConfigSpace/hyperparameters/numerical.pxd b/ConfigSpace/hyperparameters/numerical.pxd deleted file mode 100644 index 7984d06c..00000000 --- a/ConfigSpace/hyperparameters/numerical.pxd +++ /dev/null @@ -1,25 +0,0 @@ -import numpy as np -cimport numpy as np -np.import_array() - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t - -from .hyperparameter cimport Hyperparameter - - -cdef class NumericalHyperparameter(Hyperparameter): - cdef public lower - cdef public upper - cdef public q - cdef public log - cdef public _lower - cdef public _upper - cpdef int compare(self, value: Union[int, float, str], value2: Union[int, float, str]) - cpdef int compare_vector(self, DTYPE_t value, DTYPE_t value2) diff --git a/ConfigSpace/hyperparameters/numerical.py b/ConfigSpace/hyperparameters/numerical.py deleted file mode 100644 index 9875ebad..00000000 --- a/ConfigSpace/hyperparameters/numerical.py +++ /dev/null @@ -1,78 +0,0 @@ -from __future__ import annotations - -from typing import Any - -import numpy as np - -from ConfigSpace.hyperparameters.hyperparameter import Comparison, Hyperparameter - - -class NumericalHyperparameter(Hyperparameter): - def __init__(self, name: str, default_value: Any, meta: dict | None) -> None: - super().__init__(name, meta) - self.default_value = default_value - - def has_neighbors(self) -> bool: - return True - - def get_num_neighbors(self, value=None) -> float: - return np.inf - - def compare(self, value: int | float, value2: int | float) -> Comparison: - if value < value2: - return Comparison.LESS_THAN - if value > value2: - return Comparison.GREATER_THAN - - return Comparison.EQUAL - - def compare_vector(self, value: float, value2: float) -> Comparison: - if value < value2: - return Comparison.LESS_THAN - - if value > value2: - return Comparison.GREATER_THAN - - return Comparison.EQUAL - - def allow_greater_less_comparison(self) -> bool: - return True - - def __eq__(self, other: Any) -> bool: - """ - This method implements a comparison between self and another - object. - - Additionally, it defines the __ne__() as stated in the - documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result - unless it is NotImplemented. - - """ - if not isinstance(other, self.__class__): - return False - - return ( - self.name == other.name - and self.default_value == other.default_value - and self.lower == other.lower - and self.upper == other.upper - and self.log == other.log - and self.q == other.q - ) - - def __hash__(self): - return hash((self.name, self.lower, self.upper, self.log, self.q)) - - def __copy__(self): - return self.__class__( - name=self.name, - default_value=self.default_value, - lower=self.lower, - upper=self.upper, - log=self.log, - q=self.q, - meta=self.meta, - ) diff --git a/ConfigSpace/hyperparameters/numerical_hyperparameter.py b/ConfigSpace/hyperparameters/numerical_hyperparameter.py new file mode 100644 index 00000000..7ae5c523 --- /dev/null +++ b/ConfigSpace/hyperparameters/numerical_hyperparameter.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import field +from typing import Protocol, TypeVar, runtime_checkable + +import numpy as np + +NumberDType = TypeVar("NumberDType", bound=np.number) + + +@runtime_checkable +class NumericalHyperparameter(Protocol[NumberDType]): + lower: NumberDType = field(hash=True) + upper: NumberDType = field(hash=True) + log: bool = field(hash=True) diff --git a/ConfigSpace/hyperparameters/ordinal.py b/ConfigSpace/hyperparameters/ordinal.py index 8f7d4075..312e9abf 100644 --- a/ConfigSpace/hyperparameters/ordinal.py +++ b/ConfigSpace/hyperparameters/ordinal.py @@ -1,336 +1,73 @@ from __future__ import annotations -import copy -import io -from typing import Any +from collections.abc import Hashable, Mapping, Sequence +from dataclasses import dataclass, field +from functools import partial +from typing import Any, ClassVar import numpy as np +from scipy.stats import randint -from ConfigSpace.hyperparameters.hyperparameter import Comparison, Hyperparameter +from ConfigSpace.hyperparameters._distributions import ScipyDiscreteDistribution +from ConfigSpace.hyperparameters._hp_components import ( + TransformerSeq, + ordinal_neighborhood, +) +from ConfigSpace.hyperparameters.hyperparameter import Hyperparameter -class OrdinalHyperparameter(Hyperparameter): +@dataclass(init=False) +class OrdinalHyperparameter(Hyperparameter[Any, np.int64]): + orderable: ClassVar[bool] = True + sequence: Sequence[Any] = field(hash=True) + def __init__( self, name: str, - sequence: list[float | int | str] | tuple[float | int | str], - default_value: str | int | float | None = None, - meta: dict | None = None, + sequence: Sequence[Any], + default_value: Any | None = None, + meta: Mapping[Hashable, Any] | None = None, ) -> None: - """ - An ordinal hyperparameter. - - Its values are sampled form a ``sequence`` of values. - The sequence of values from a ordinal hyperparameter is ordered. - - ``None`` is a forbidden value, please use a string constant instead and parse - it in your own code, see `here `_ - for further details. - - >>> from ConfigSpace import OrdinalHyperparameter - >>> - >>> OrdinalHyperparameter('o', sequence=['10', '20', '30']) - o, Type: Ordinal, Sequence: {10, 20, 30}, Default: 10 - - Parameters - ---------- - name : str - Name of the hyperparameter, with which it can be accessed. - sequence : list or tuple with (str, float, int) - ordered collection of values to sample hyperparameter from. - default_value : int, float, str, optional - Sets the default value of a hyperparameter to a given value. - meta : Dict, optional - Field for holding meta data provided by the user. - Not used by the configuration space. - """ - # Remark - # Since the sequence can consist of elements from different types, - # they are stored into a dictionary in order to handle them as a - # numeric sequence according to their order/position. - super().__init__(name, meta) - if len(sequence) > len(set(sequence)): + self.sequence = sequence + size = len(sequence) + if default_value is None: + default_value = self.sequence[0] + elif default_value not in sequence: raise ValueError( - "Ordinal Hyperparameter Sequence %s contain duplicate values." % sequence, + "The default value has to be one of the ordinal values. " + f"Got {default_value!r} which is not in {sequence}.", ) - self.sequence = tuple(sequence) - self.num_elements = len(sequence) - self.sequence_vector = list(range(self.num_elements)) - self.default_value = self.check_default(default_value) - self.normalized_default_value = self._inverse_transform(self.default_value) - self.value_dict = {e: i for i, e in enumerate(self.sequence)} - - def __hash__(self): - return hash((self.name, self.sequence)) - - def __repr__(self) -> str: - """Write out the parameter definition.""" - repr_str = io.StringIO() - repr_str.write("%s, Type: Ordinal, Sequence: {" % (self.name)) - for idx, seq in enumerate(self.sequence): - repr_str.write(str(seq)) - if idx < len(self.sequence) - 1: - repr_str.write(", ") - repr_str.write("}") - repr_str.write(", Default: ") - repr_str.write(str(self.default_value)) - repr_str.seek(0) - return repr_str.getvalue() - - def __eq__(self, other: Any) -> bool: - """Comparison between self and another object. - - Additionally, it defines the __ne__() as stated in the - documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result - unless it is NotImplemented. - - """ - if not isinstance(other, self.__class__): - return False - - return ( - self.name == other.name - and self.sequence == other.sequence - and self.default_value == other.default_value - ) - - def __copy__(self): - return OrdinalHyperparameter( - name=self.name, - sequence=copy.deepcopy(self.sequence), - default_value=self.default_value, - meta=self.meta, - ) - - def compare(self, value: int | float | str, value2: int | float | str) -> Comparison: - if self.value_dict[value] < self.value_dict[value2]: - return Comparison.LESS_THAN - - if self.value_dict[value] > self.value_dict[value2]: - return Comparison.GREATER_THAN - - return Comparison.EQUAL - - def compare_vector(self, value, value2) -> Comparison: - if value < value2: - return Comparison.LESS_THAN - if value > value2: - return Comparison.GREATER_THAN - - return Comparison.EQUAL - - def is_legal(self, value: int | float | str) -> bool: - """Check if a certain value is represented in the sequence.""" - return value in self.sequence - - def is_legal_vector(self, value) -> int: - return value in self.sequence_vector - - def check_default( - self, - default_value: int | float | str | None, - ) -> int | float | str: - """ - check if given default value is represented in the sequence. - If there's no default value we simply choose the - first element in our sequence as default. - """ - if default_value is None: - return self.sequence[0] - elif self.is_legal(default_value): - return default_value - else: - raise ValueError("Illegal default value %s" % str(default_value)) - - def _transform_vector(self, vector: np.ndarray) -> np.ndarray: - if np.isnan(vector).any(): - raise ValueError("Vector %s contains NaN's" % vector) - - if np.equal(np.mod(vector, 1), 0): - return self.sequence[vector.astype(int)] - - raise ValueError( - "Can only index the choices of the ordinal " - f"hyperparameter {self} with an integer, but provided " - f"the following float: {vector:f}", - ) - - def _transform_scalar(self, scalar: float | int) -> float | int | str: - if scalar != scalar: - raise ValueError("Number %s is NaN" % scalar) - if scalar % 1 == 0: - return self.sequence[int(scalar)] - - raise ValueError( - "Can only index the choices of the ordinal " - f"hyperparameter {self} with an integer, but provided " - f"the following float: {scalar:f}", + super().__init__( + name=name, + size=size, + default_value=default_value, + meta=meta, + transformer=TransformerSeq(seq=sequence), + neighborhood=partial(ordinal_neighborhood, size=int(size)), + vector_dist=ScipyDiscreteDistribution( + rv=randint(a=0, b=size), # type: ignore + max_density_value=1 / size, + dtype=np.int64, + ), + neighborhood_size=self._neighborhood_size, ) - def _transform( - self, - vector: np.ndarray | float | int, - ) -> np.ndarray | float | int | None: - try: - if isinstance(vector, np.ndarray): - return self._transform_vector(vector) - return self._transform_scalar(vector) - except ValueError: - return None - - def _inverse_transform( - self, - vector: np.ndarray | list | int | str | float | None, - ) -> float | list[int] | list[str] | list[float]: - if vector is None: - return np.NaN - return self.sequence.index(vector) - - def get_seq_order(self) -> np.ndarray: - """ - return the ordinal sequence as numeric sequence - (according to the the ordering) from 1 to length of our sequence. - """ - return np.arange(0, self.num_elements) + def _neighborhood_size(self, value: Any | None) -> int: + size = len(self.sequence) + if value is None: + return size - def get_order(self, value: int | str | float | None) -> int: - """Return the seuence position/order of a certain value from the sequence.""" - return self.value_dict[value] - - def get_value(self, idx: int) -> int | str | float: - """Return the sequence value of a given order/position.""" - return list(self.value_dict.keys())[list(self.value_dict.values()).index(idx)] - - def check_order(self, val1: int | str | float, val2: int | str | float) -> bool: - """Check whether value1 is smaller than value2.""" - idx1 = self.get_order(val1) - idx2 = self.get_order(val2) - return idx1 < idx2 - - def _sample(self, rs: np.random.RandomState, size: int | None = None) -> int: - """Return a random sample from our sequence as order/position index.""" - return rs.randint(0, self.num_elements, size=size) - - def has_neighbors(self) -> bool: - """ - check if there are neighbors or we're only dealing with an - one-element sequence. - """ - return len(self.sequence) > 1 - - def get_num_neighbors(self, value: int | float | str) -> int: - """Return the number of existing neighbors in the sequence.""" - max_idx = len(self.sequence) - 1 - # check if there is only one value - if value == self.sequence[0] and value == self.sequence[max_idx]: + # No neighbors if it's the only element + if size == 1: return 0 - elif value in (self.sequence[0], self.sequence[max_idx]): - return 1 - else: - return 2 - - def get_neighbors( - self, - value: int | str | float, - rs: None, - number: int = 0, - transform: bool = False, - ) -> list[str | float | int]: - """ - Return the neighbors of a given value. - Value must be in vector form. Ordinal name will not work. - """ - neighbors = [] - if transform: - if self.get_num_neighbors(value) < len(self.sequence): - index = self.get_order(value) - neighbor_idx1 = index - 1 - neighbor_idx2 = index + 1 - seq = self.get_seq_order() - - if neighbor_idx1 >= seq[0]: - candidate1 = self.get_value(neighbor_idx1) - if self.check_order(candidate1, value): - neighbors.append(candidate1) - if neighbor_idx2 < self.num_elements: - candidate2 = self.get_value(neighbor_idx2) - if self.check_order(value, candidate2): - neighbors.append(candidate2) - - else: - if self.get_num_neighbors(self.get_value(value)) < len(self.sequence): - index = value - neighbor_idx1 = index - 1 - neighbor_idx2 = index + 1 - seq = self.get_seq_order() - - if neighbor_idx1 < index and neighbor_idx1 >= seq[0]: - neighbors.append(neighbor_idx1) - if neighbor_idx2 > index and neighbor_idx2 < self.num_elements: - neighbors.append(neighbor_idx2) - - return neighbors - - def allow_greater_less_comparison(self) -> bool: - return True - - def pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the hyperparameter in - the original hyperparameter space (the one specified by the user). - For each parameter type, there is also a method _pdf which - operates on the transformed (and possibly normalized) hyperparameter - space. Only legal values return a positive probability density, - otherwise zero. The OrdinalHyperparameter is treated - as a UniformHyperparameter with regard to its probability density. - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. + end_index = len(self.sequence) - 1 + index = self.sequence.index(value) - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - if vector.ndim != 1: - raise ValueError("Method pdf expects a one-dimensional numpy array") - return self._pdf(vector) - - def _pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the hyperparameter in - the transformed (and possibly normalized, depends on the hyperparameter - type) space. As such, one never has to worry about log-normal - distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). The OrdinalHyperparameter is treated - as a UniformHyperparameter with regard to its probability density. - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - if not np.all(np.isin(vector, self.sequence)): - raise ValueError( - f"Some element in the vector {vector} is not in the sequence {self.sequence}.", - ) - return np.ones_like(vector, dtype=np.float64) / self.num_elements - - def get_max_density(self) -> float: - return 1 / self.num_elements + # We have at least 2 elements + if index in (0, end_index): + return 1 - def get_size(self) -> float: - return len(self.sequence) + # We have at least 3 elements and the value is not at the ends + return 2 diff --git a/ConfigSpace/hyperparameters/uniform_float.pxd b/ConfigSpace/hyperparameters/uniform_float.pxd deleted file mode 100644 index 65017c64..00000000 --- a/ConfigSpace/hyperparameters/uniform_float.pxd +++ /dev/null @@ -1,18 +0,0 @@ -import numpy as np -cimport numpy as np -np.import_array() - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t - -from ConfigSpace.hyperparameters.float_hyperparameter cimport FloatHyperparameter - - -cdef class UniformFloatHyperparameter(FloatHyperparameter): - pass diff --git a/ConfigSpace/hyperparameters/uniform_float.py b/ConfigSpace/hyperparameters/uniform_float.py index 6a1567a6..ee740a12 100644 --- a/ConfigSpace/hyperparameters/uniform_float.py +++ b/ConfigSpace/hyperparameters/uniform_float.py @@ -1,287 +1,73 @@ from __future__ import annotations -import io -from typing import TYPE_CHECKING, overload +import math +from collections.abc import Hashable, Mapping +from dataclasses import dataclass +from typing import ( + Any, + ClassVar, +) import numpy as np -import numpy.typing as npt +from scipy.stats import ( + uniform, +) -from ConfigSpace.deprecate import deprecate +from ConfigSpace.hyperparameters._distributions import ScipyContinuousDistribution +from ConfigSpace.hyperparameters._hp_components import ROUND_PLACES, UnitScaler from ConfigSpace.hyperparameters.float_hyperparameter import FloatHyperparameter - -if TYPE_CHECKING: - from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter +from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter +@dataclass(init=False) class UniformFloatHyperparameter(FloatHyperparameter): + orderable: ClassVar[bool] = True + def __init__( self, name: str, - lower: int | float, - upper: int | float, - default_value: int | float | None = None, - q: int | float | None = None, + lower: int | float | np.floating, + upper: int | float | np.floating, + default_value: int | float | np.floating | None = None, log: bool = False, - meta: dict | None = None, + meta: Mapping[Hashable, Any] | None = None, ) -> None: - """ - A uniformly distributed float hyperparameter. - - Its values are sampled from a uniform distribution with values - from ``lower`` to ``upper``. - - >>> from ConfigSpace import UniformFloatHyperparameter - >>> - >>> UniformFloatHyperparameter('u', lower=10, upper=100, log = False) - u, Type: UniformFloat, Range: [10.0, 100.0], Default: 55.0 - - Parameters - ---------- - name : str - Name of the hyperparameter, with which it can be accessed - lower : int, float - Lower bound of a range of values from which the hyperparameter will be sampled - upper : int, float - Upper bound - default_value : int, float, optional - Sets the default value of a hyperparameter to a given value - q : int, float, optional - Quantization factor - log : bool, optional - If ``True``, the values of the hyperparameter will be sampled - on a logarithmic scale. Default to ``False`` - meta : Dict, optional - Field for holding meta data provided by the user. - Not used by the configuration space. - """ - lower = float(lower) - upper = float(upper) - q = float(q) if q is not None else None - log = bool(log) - if lower >= upper: - raise ValueError( - f"Upper bound {upper:f} must be larger than lower bound " - f"{lower:f} for hyperparameter {name}", - ) - - if log and lower <= 0: - raise ValueError( - f"Negative lower bound ({lower:f}) for log-scale " - f"hyperparameter {name} is forbidden.", - ) - - if q is not None and (np.round((upper - lower) % q, 10) not in (0, q)): - # There can be weird rounding errors, so we compare the result against self.q - # for example, 2.4 % 0.2 = 0.1999999999999998 - diff = upper - lower - raise ValueError( - f"Upper bound minus lower bound ({upper:f} - {lower:f} = {diff}) must be" - f" a multiple of q ({q})", - ) - - self.lower = lower - self.upper = upper - self.q = q + self.lower = np.float64(lower) + self.upper = np.float64(upper) self.log = log - q_lower, q_upper = (lower, upper) - if q is not None: - q_lower = lower - (q / 2.0 - 0.0001) - q_upper = upper + (q / 2.0 - 0.0001) - - if log: - self._lower = np.log(q_lower) - self._upper = np.log(q_upper) - else: - self._lower = q_lower - self._upper = q_upper + try: + scaler = UnitScaler(self.lower, self.upper, log=log) + except ValueError as e: + raise ValueError(f"Hyperparameter '{name}' has illegal settings") from e if default_value is None: - if log: - default_value = float(np.exp((np.log(lower) + np.log(upper)) / 2.0)) - else: - default_value = (lower + upper) / 2.0 - - default_value = float(np.round(default_value, 10)) - if not self.is_legal(default_value): - raise ValueError(f"Illegal default value {default_value}") - - self.default_value = default_value - self.normalized_default_value = self._inverse_transform(self.default_value) - super().__init__(name=name, default_value=default_value, meta=meta) - - def is_legal(self, value: float, *, normalized: bool = False) -> bool: - if not isinstance(value, (float, int)): - return False - - if normalized: - return 0.0 <= value <= 1.0 - - return self.lower <= value <= self.upper + _default_value = np.float64(scaler.to_value(np.array([0.5]))[0]) + else: + _default_value = np.float64(round(default_value, ROUND_PLACES)) - def is_legal_vector(self, value: float) -> bool: - # NOTE: This really needs a better name as it doesn't operate on vectors, - # rather individual values. - # it means that it operates on the normalzied space, i.e. what is gotten - # by inverse_transform - deprecate(self.is_legal_vector, "Please use is_legal(..., normalized=True) instead") - return 1.0 >= value >= 0.0 + vect_dist = ScipyContinuousDistribution( + rv=uniform(a=0, b=1), # type: ignore + max_density_value=float(1 / (self.upper - self.lower)), + dtype=np.float64, + ) + super().__init__( + name=name, + size=np.inf, + default_value=_default_value, + meta=meta, + transformer=scaler, + neighborhood=vect_dist.neighborhood, + vector_dist=vect_dist, + neighborhood_size=np.inf, + ) def to_integer(self) -> UniformIntegerHyperparameter: - from ConfigSpace.hyperparameters.uniform_integer import UniformIntegerHyperparameter - return UniformIntegerHyperparameter( name=self.name, - lower=int(np.ceil(self.lower)), - upper=int(np.floor(self.upper)), - default_value=int(np.rint(self.default_value)), - q=int(np.rint(self.q)) if self.q is not None else None, + lower=math.ceil(self.lower), + upper=math.floor(self.upper), + default_value=round(self.default_value), log=self.log, meta=None, ) - - @overload - def _sample(self, rs: np.random.RandomState, size: None = None) -> float: - ... - - @overload - def _sample(self, rs: np.random.RandomState, size: int) -> npt.NDArray[np.float64]: - ... - - def _sample( - self, - rs: np.random.RandomState, - size: int | None = None, - ) -> float | npt.NDArray[np.float64]: - return rs.uniform(size=size) - - def _transform_scalar(self, scalar: float) -> np.float64: - deprecate(self._transform_scalar, "Please use _transform instead") - return self._transform(scalar) - - def _transform_vector(self, vector: npt.NDArray[np.number]) -> npt.NDArray[np.float64]: - deprecate(self._transform_scalar, "Please use _transform instead") - return self._transform(vector) - - @overload - def _transform(self, vector: float) -> np.float64: - ... - - @overload - def _transform(self, vector: npt.NDArray[np.number]) -> npt.NDArray[np.float64]: - ... - - def _transform( - self, - vector: npt.NDArray[np.number] | float, - ) -> npt.NDArray[np.float64] | np.float64: - vector = vector * (self._upper - self._lower) + self._lower - if self.log: - vector = np.exp(vector, dtype=np.float64) - - if self.q is not None: - quantized = (vector - self.lower) / self.q - vector = np.rint(quantized) * self.q + self.lower - - return np.clip(vector, self.lower, self.upper, dtype=np.float64) - - @overload - def _inverse_transform(self, vector: float) -> np.float64: - ... - - @overload - def _inverse_transform(self, vector: npt.NDArray[np.number]) -> npt.NDArray[np.float64]: - ... - - def _inverse_transform( - self, - vector: npt.NDArray[np.number] | float, - ) -> npt.NDArray[np.float64] | np.float64: - """Converts a value from the original space to the transformed space (0, 1).""" - if self.log: - vector = np.log(vector) - vector = (vector - self._lower) / (self._upper - self._lower) - return np.clip(vector, 0.0, 1.0, dtype=np.float64) - - def get_neighbors( - self, - value: float, # Should be normalized closely into 0, 1! - rs: np.random.RandomState, - number: int = 4, - transform: bool = False, - std: float = 0.2, - ) -> npt.NDArray[np.float64]: - BUFFER_MULTIPLIER = 2 - SAMPLE_SIZE = number * BUFFER_MULTIPLIER - # Make sure we can accomidate the number of (neighbors - 1) + a new sample set - BUFFER_SIZE = number + number * BUFFER_MULTIPLIER - - neighbors = np.empty(BUFFER_SIZE, dtype=np.float64) - offset = 0 - - # Generate batches of number * 2 candidates, filling the above - # buffer until we have enough valid candidates. - # We should not overflow as the buffer - while offset < number: - candidates = rs.normal(value, std, size=(SAMPLE_SIZE,)) - valid_candidates = candidates[(candidates >= 0) & (candidates <= 1)] - - n_candidates = len(valid_candidates) - neighbors[offset:n_candidates] = valid_candidates - offset += n_candidates - - neighbors = neighbors[:number] - if transform: - return self._transform(neighbors) - - return neighbors - - def _pdf(self, vector: npt.NDArray[np.number]) -> npt.NDArray[np.float64]: - """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal - distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - # everything that comes into _pdf for a uniform variable should - # already be in [0, 1]-range, and if not, it's outside the upper - # or lower bound. - ub = 1 - lb = 0 - inside_range = ((lb <= vector) & (vector <= ub)).astype(dtype=np.uint64) - return np.true_divide(inside_range, self.upper - self.lower, dtype=np.float64) - - def get_max_density(self) -> float: - return 1 / (self.upper - self.lower) - - def get_size(self) -> float: - if self.q is None: - return np.inf - - return np.rint((self.upper - self.lower) / self.q) + 1 - - def __repr__(self) -> str: - repr_str = io.StringIO() - repr_str.write( - f"{self.name}, Type: UniformFloat, " - f"Range: [{self.lower!r}, {self.upper!r}], " - f"Default: {self.default_value!r}", - ) - if self.log: - repr_str.write(", on log-scale") - if self.q is not None: - repr_str.write(", Q: %s" % str(self.q)) - repr_str.seek(0) - return repr_str.getvalue() diff --git a/ConfigSpace/hyperparameters/uniform_integer.pxd b/ConfigSpace/hyperparameters/uniform_integer.pxd deleted file mode 100644 index 3285a180..00000000 --- a/ConfigSpace/hyperparameters/uniform_integer.pxd +++ /dev/null @@ -1,18 +0,0 @@ -import numpy as np -cimport numpy as np -np.import_array() - -# We now need to fix a datatype for our arrays. I've used the variable -# DTYPE for this, which is assigned to the usual NumPy runtime -# type info object. -DTYPE = float -# "ctypedef" assigns a corresponding compile-time type to DTYPE_t. For -# every type in the numpy module there's a corresponding compile-time -# type with a _t-suffix. -ctypedef np.float_t DTYPE_t - -from ConfigSpace.hyperparameters.integer_hyperparameter cimport IntegerHyperparameter - - -cdef class UniformIntegerHyperparameter(IntegerHyperparameter): - pass diff --git a/ConfigSpace/hyperparameters/uniform_integer.py b/ConfigSpace/hyperparameters/uniform_integer.py index ac841a12..14f02035 100644 --- a/ConfigSpace/hyperparameters/uniform_integer.py +++ b/ConfigSpace/hyperparameters/uniform_integer.py @@ -1,335 +1,75 @@ from __future__ import annotations -import io +from collections.abc import Hashable, Mapping +from dataclasses import dataclass +from typing import ( + Any, + ClassVar, +) import numpy as np - -from ConfigSpace.functional import center_range +from scipy.stats import ( + uniform, +) + +from ConfigSpace.hyperparameters._distributions import ( + DiscretizedContinuousScipyDistribution, +) +from ConfigSpace.hyperparameters._hp_components import ( + VECTORIZED_NUMERIC_LOWER, + VECTORIZED_NUMERIC_UPPER, + UnitScaler, +) from ConfigSpace.hyperparameters.integer_hyperparameter import IntegerHyperparameter -from ConfigSpace.hyperparameters.uniform_float import UniformFloatHyperparameter +@dataclass(init=False) class UniformIntegerHyperparameter(IntegerHyperparameter): + orderable: ClassVar[bool] = True + def __init__( self, name: str, - lower: int | float, - upper: int | float, - default_value: int | None = None, - q: int | None = None, + lower: int | float | np.number, + upper: int | float | np.number, + default_value: int | np.integer | None = None, log: bool = False, - meta: dict | None = None, + meta: Mapping[Hashable, Any] | None = None, ) -> None: - """ - A uniformly distributed integer hyperparameter. - - Its values are sampled from a uniform distribution - with bounds ``lower`` and ``upper``. - - >>> from ConfigSpace import UniformIntegerHyperparameter - >>> - >>> UniformIntegerHyperparameter(name='u', lower=10, upper=100, log=False) - u, Type: UniformInteger, Range: [10, 100], Default: 55 - - Parameters - ---------- - name : str - Name of the hyperparameter with which it can be accessed - lower : int - Lower bound of a range of values from which the hyperparameter will be sampled - upper : int - upper bound - default_value : int, optional - Sets the default value of a hyperparameter to a given value - q : int, optional - Quantization factor - log : bool, optional - If ``True``, the values of the hyperparameter will be sampled - on a logarithmic scale. Defaults to ``False`` - meta : Dict, optional - Field for holding meta data provided by the user. - Not used by the configuration space. - """ + self.lower = np.int64(np.rint(lower)) + self.upper = np.int64(np.rint(upper)) self.log = bool(log) - self.lower = self.check_int(lower, "lower") - self.upper = self.check_int(upper, "upper") - - self.q = None - if q is not None: - if q < 1: - raise ValueError( - "Setting quantization < 1 for Integer " - f"Hyperparameter {name} has no effect.", - ) - - self.q = self.check_int(q, "q") - if (self.upper - self.lower) % self.q != 0: - raise ValueError( - "Upper bound (%d) - lower bound (%d) must be a multiple of q (%d)" - % (self.upper, self.lower, self.q), - ) - - if self.lower >= self.upper: - raise ValueError( - "Upper bound %d must be larger than lower bound " - "%d for hyperparameter %s" % (self.lower, self.upper, name), - ) - if log and self.lower <= 0: - raise ValueError( - "Negative lower bound (%d) for log-scale " - "hyperparameter %s is forbidden." % (self.lower, name), - ) - - # Requires `log` to be set first - if default_value is not None: - default_value = self.check_int(default_value, name) - else: - default_value = self.check_default(default_value) - - # NOTE: Placed after the default value check to ensure it's set and not None - super().__init__(name, default_value, meta) - - self.ufhp = UniformFloatHyperparameter( - self.name, - self.lower - 0.49999, - self.upper + 0.49999, - log=self.log, - default_value=self.default_value, - ) - - self.normalized_default_value = self._inverse_transform(self.default_value) - def __repr__(self) -> str: - repr_str = io.StringIO() - repr_str.write( - f"{self.name}, Type: UniformInteger," - f" Range: [{self.lower!r}, {self.upper!r}]," - f" Default: {self.default_value!r}", - ) - if self.log: - repr_str.write(", on log-scale") - if self.q is not None: - repr_str.write(f", Q: {self.q}") - repr_str.seek(0) - return repr_str.getvalue() - - def _sample( - self, - rs: np.random.RandomState, - size: int | None = None, - ) -> np.ndarray | float: - value = self.ufhp._sample(rs, size=size) - # Map all floats which belong to the same integer value to the same - # float value by first transforming it to an integer and then - # transforming it back to a float between zero and one - value = self._transform(value) - return self._inverse_transform(value) - - def _transform_vector(self, vector: np.ndarray) -> np.ndarray: - vector = self.ufhp._transform_vector(vector) - if self.q is not None: - vector = np.rint((vector - self.lower) / self.q) * self.q + self.lower - vector = np.minimum(vector, self.upper) - vector = np.maximum(vector, self.lower) - - return np.rint(vector) - - def _transform_scalar(self, scalar: float) -> float: - scalar = self.ufhp._transform_scalar(scalar) - if self.q is not None: - scalar = np.round((scalar - self.lower) / self.q) * self.q + self.lower - scalar = min(scalar, self.upper) - scalar = max(scalar, self.lower) - return int(np.round(scalar)) - - def _inverse_transform( - self, - vector: np.ndarray | float | int, - ) -> np.ndarray | float | int: - return self.ufhp._inverse_transform(vector) - - def is_legal(self, value: int) -> bool: - if not (isinstance(value, (int, np.int32, np.int64))): - return False - return self.upper >= value >= self.lower + try: + scaler = UnitScaler(lower, upper, log=log) + except ValueError as e: + raise ValueError(f"Hyperparameter '{name}' has illegal settings") from e - def is_legal_vector(self, value) -> int: - return 1.0 >= value >= 0.0 - - def check_default(self, default_value: int | float | None) -> int: - # Doesn't seem to quantize with q? if default_value is None: - if self.log: - default_value = np.exp((np.log(self.lower) + np.log(self.upper)) / 2.0) - else: - default_value = (self.lower + self.upper) / 2.0 - default_value = int(np.round(default_value, 0)) - - if self.is_legal(default_value): - return default_value - - raise ValueError("Illegal default value %s" % str(default_value)) - - def has_neighbors(self) -> bool: - if self.log: - upper = np.exp(self.ufhp._upper) - lower = np.exp(self.ufhp._lower) + _default_value = np.int64(scaler.to_value(np.array([0.5]))[0]) else: - upper = self.ufhp._upper - lower = self.ufhp._lower - - # If there is only one active value, this is not enough - return upper - lower >= 1 - - def get_num_neighbors(self, value: int | None = None) -> int: - # If there is a value in the range, then that value is not a neighbor of itself - # so we need to remove one - if value is not None and self.lower <= value <= self.upper: - return self.upper - self.lower - 1 - - return self.upper - self.lower - - def get_neighbors( - self, - value: float, - rs: np.random.RandomState, - number: int = 4, - transform: bool = False, - std: float = 0.2, - ) -> list[int]: - """Get the neighbors of a value. - - NOTE - ---- - **This assumes the value is in the unit-hypercube [0, 1]** - - Parameters - ---------- - value: float - The value to get neighbors around. This assume the ``value`` has been - converted to the [0, 1] range which can be done with ``_inverse_transform``. - - rs: RandomState - The random state to use - - number: int = 4 - How many neighbors to get - - transform: bool = False - Whether to transform this value from the unit cube, back to the - hyperparameter's specified range of values. - - std: float = 0.2 - The std. dev. to use in the [0, 1] hypercube space while sampling - for neighbors. - - Returns - ------- - List[int] - Some ``number`` of neighbors centered around ``value``. - """ - assert 0 <= value <= 1, ( - "For get neighbors of UniformIntegerHyperparameter, the value" - " if assumed to be in the unit-hypercube [0, 1]. If this was not" - " the behaviour assumed, please raise a ticket on github." + _default_value = np.int64(round(default_value)) + + self.log = log + size = self.upper - self.lower + 1 + vector_dist = DiscretizedContinuousScipyDistribution( + dist=uniform(VECTORIZED_NUMERIC_LOWER, VECTORIZED_NUMERIC_UPPER), # type: ignore + steps=size, + max_density_value=float(1 / size), + normalization_constant_value=1, ) - assert number < 1000000, "Can only generate less than 1 million neighbors." - # Convert python values to cython ones - center = self._transform(value) - lower = self.lower - upper = self.upper - n_requested = number - n_neighbors = upper - lower - 1 - stepsize = self.q if self.q is not None else 1 - - neighbors = [] - - v: int # A value that's possible to return - if n_neighbors < n_requested: - for v in range(lower, center): - neighbors.append(v) - - for v in range(center + 1, upper + 1): - neighbors.append(v) - - if transform: - return neighbors - - return self._inverse_transform(np.asarray(neighbors)).tolist() - - # A truncated normal between 0 and 1, centered on the value with a scale of std. - # This will be sampled from and converted to the corresponding int value - # However, this is too slow - we use the "poor man's truncnorm below" - # cdef np.ndarray float_indices = truncnorm.rvs( - # We sample five times as many values as needed and weed them out below - # (perform rejection sampling and make sure we don't sample any neighbor twice) - # This increases our chances of not having to fill the neighbors list by calling - # `center_range` - # Five is an arbitrary number and can probably be tuned to reduce overhead - float_indices: np.ndarray = rs.normal(value, std, size=number * 5) - mask: np.ndarray = (float_indices >= 0) & (float_indices <= 1) - float_indices = float_indices[mask] - - possible_neighbors_as_array: np.ndarray = self._transform_vector(float_indices).astype( - np.longlong, + super().__init__( + name=name, + size=int(size), + default_value=_default_value, + meta=meta, + transformer=scaler, + vector_dist=vector_dist, + neighborhood=vector_dist.neighborhood, + neighborhood_size=self._neighborhood_size, ) - possible_neighbors: np.ndarray = possible_neighbors_as_array - - n_neighbors_generated: int = 0 - n_candidates: int = len(float_indices) - candidate_index: int = 0 - seen: set[int] = {center} - while n_neighbors_generated < n_requested and candidate_index < n_candidates: - v = possible_neighbors[candidate_index] - if v not in seen: - seen.add(v) - n_neighbors_generated += 1 - candidate_index += 1 - - if n_neighbors_generated < n_requested: - numbers_around = center_range(center, lower, upper, stepsize) - - while n_neighbors_generated < n_requested: - v = next(numbers_around) - if v not in seen: - seen.add(v) - n_neighbors_generated += 1 - - seen.remove(center) - neighbors = list(seen) - if transform: - return neighbors - - return self._inverse_transform(np.array(neighbors)).tolist() - - def _pdf(self, vector: np.ndarray) -> np.ndarray: - """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal - distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). Optimally, an IntegerHyperparameter - should have a corresponding float, which can be utlized for the calls - to the probability density function (see e.g. NormalIntegerHyperparameter). - - Parameters - ---------- - vector: np.ndarray - the (N, ) vector of inputs for which the probability density - function is to be computed. - - Returns - ------- - np.ndarray(N, ) - Probability density values of the input vector - """ - return self.ufhp._pdf(vector) - - def get_max_density(self) -> float: - lb = self.lower - ub = self.upper - return 1 / (ub - lb + 1) - def get_size(self) -> float: - q = 1 if self.q is None else self.q - return np.rint((self.upper - self.lower) / q) + 1 + def neighborhood_size(self, value: np.int64 | None) -> int | float: + if value is None or self.lower <= value <= self.upper: + return self.size + return self.size - 1 diff --git a/ConfigSpace/read_and_write/json.py b/ConfigSpace/read_and_write/json.py index a922042e..31fcff50 100644 --- a/ConfigSpace/read_and_write/json.py +++ b/ConfigSpace/read_and_write/json.py @@ -68,7 +68,6 @@ def _build_uniform_float(param: UniformFloatHyperparameter) -> dict: "lower": param.lower, "upper": param.upper, "default": param.default_value, - "q": param.q, } @@ -82,7 +81,6 @@ def _build_normal_float(param: NormalFloatHyperparameter) -> dict: "default": param.default_value, "lower": param.lower, "upper": param.upper, - "q": param.q, } @@ -96,7 +94,6 @@ def _build_beta_float(param: BetaFloatHyperparameter) -> dict: "lower": param.lower, "upper": param.upper, "default": param.default_value, - "q": param.q, } @@ -108,7 +105,6 @@ def _build_uniform_int(param: UniformIntegerHyperparameter) -> dict: "lower": param.lower, "upper": param.upper, "default": param.default_value, - "q": param.q, } @@ -122,7 +118,6 @@ def _build_normal_int(param: NormalIntegerHyperparameter) -> dict: "lower": param.lower, "upper": param.upper, "default": param.default_value, - "q": param.q, } @@ -136,7 +131,6 @@ def _build_beta_int(param: BetaIntegerHyperparameter) -> dict: "lower": param.lower, "upper": param.upper, "default": param.default_value, - "q": param.q, } @@ -317,8 +311,7 @@ def _build_forbidden_relation(clause: ForbiddenRelation) -> dict: ################################################################################ def write(configuration_space: ConfigurationSpace, indent: int = 2) -> str: - """ - Create a string representation of a + """Create a string representation of a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` in json format. This string can be written to file. @@ -339,7 +332,7 @@ def write(configuration_space: ConfigurationSpace, indent: int = 2) -> str: indent : int number of whitespaces to use as indent - Returns + Returns: ------- str String representation of the configuration space, @@ -401,8 +394,7 @@ def write(configuration_space: ConfigurationSpace, indent: int = 2) -> str: ################################################################################ def read(jason_string: str) -> ConfigurationSpace: - """ - Create a configuration space definition from a json string. + """Create a configuration space definition from a json string. .. code:: python @@ -425,7 +417,7 @@ def read(jason_string: str) -> ConfigurationSpace: jason_string : str A json string representing a configuration space definition - Returns + Returns: ------- :class:`~ConfigSpace.configuration_space.ConfigurationSpace` The deserialized ConfigurationSpace object @@ -484,9 +476,6 @@ def _construct_hyperparameter(hyperparameter: dict) -> Hyperparameter: # noqa: lower=hyperparameter["lower"], upper=hyperparameter["upper"], default_value=hyperparameter["default"], - # Backwards compatibily issue - # https://github.com/automl/ConfigSpace/issues/325 - q=hyperparameter.get("q", None), ) if hp_type == "normal_float": @@ -498,9 +487,6 @@ def _construct_hyperparameter(hyperparameter: dict) -> Hyperparameter: # noqa: lower=hyperparameter["lower"], upper=hyperparameter["upper"], default_value=hyperparameter["default"], - # Backwards compatibily issue - # https://github.com/automl/ConfigSpace/issues/325 - q=hyperparameter.get("q", None), ) if hp_type == "beta_float": @@ -511,9 +497,6 @@ def _construct_hyperparameter(hyperparameter: dict) -> Hyperparameter: # noqa: lower=hyperparameter["lower"], upper=hyperparameter["upper"], log=hyperparameter["log"], - # Backwards compatibily issue - # https://github.com/automl/ConfigSpace/issues/325 - q=hyperparameter.get("q", None), default_value=hyperparameter["default"], ) @@ -524,9 +507,6 @@ def _construct_hyperparameter(hyperparameter: dict) -> Hyperparameter: # noqa: lower=hyperparameter["lower"], upper=hyperparameter["upper"], default_value=hyperparameter["default"], - # Backwards compatibily issue - # https://github.com/automl/ConfigSpace/issues/325 - q=hyperparameter.get("q", None), ) if hp_type == "normal_int": @@ -538,9 +518,6 @@ def _construct_hyperparameter(hyperparameter: dict) -> Hyperparameter: # noqa: lower=hyperparameter["lower"], upper=hyperparameter["upper"], default_value=hyperparameter["default"], - # Backwards compatibily issue - # https://github.com/automl/ConfigSpace/issues/325 - q=hyperparameter.get("q", None), ) if hp_type == "beta_int": @@ -551,9 +528,6 @@ def _construct_hyperparameter(hyperparameter: dict) -> Hyperparameter: # noqa: lower=hyperparameter["lower"], upper=hyperparameter["upper"], log=hyperparameter["log"], - # Backwards compatibily issue - # https://github.com/automl/ConfigSpace/issues/325 - q=hyperparameter.get("q", None), default_value=hyperparameter["default"], ) @@ -681,7 +655,10 @@ def _construct_forbidden_equals( clause: dict, cs: ConfigurationSpace, ) -> ForbiddenEqualsClause: - return ForbiddenEqualsClause(hyperparameter=cs[clause["name"]], value=clause["value"]) + return ForbiddenEqualsClause( + hyperparameter=cs[clause["name"]], + value=clause["value"], + ) def _construct_forbidden_in( diff --git a/ConfigSpace/read_and_write/pcs.py b/ConfigSpace/read_and_write/pcs.py index b30b25fe..dc526b2f 100644 --- a/ConfigSpace/read_and_write/pcs.py +++ b/ConfigSpace/read_and_write/pcs.py @@ -1,20 +1,22 @@ #!/usr/bin/env python -""" -The old PCS format is part of the `Algorithm Configuration Library `_. +"""The old PCS format is part of the `Algorithm Configuration Library `_. A detailed explanation of the **old** PCS format can be found `here. `_ """ + from __future__ import annotations +from ConfigSpace.hyperparameters.hyperparameter import HyperparameterWithPrior + __authors__ = ["Katharina Eggensperger", "Matthias Feurer"] __contact__ = "automl.org" import sys from collections import OrderedDict +from collections.abc import Iterable from io import StringIO from itertools import product -from typing import Iterable import pyparsing @@ -68,7 +70,9 @@ ) pp_digits = "0123456789" pp_plusorminus = pyparsing.Literal("+") | pyparsing.Literal("-") -pp_int = pyparsing.Combine(pyparsing.Optional(pp_plusorminus) + pyparsing.Word(pp_digits)) +pp_int = pyparsing.Combine( + pyparsing.Optional(pp_plusorminus) + pyparsing.Word(pp_digits), +) pp_float = pyparsing.Combine( pyparsing.Optional(pp_plusorminus) + pyparsing.Optional(pp_int) + "." + pp_int, ) @@ -78,7 +82,9 @@ pp_number = pp_e_notation | pp_float | pp_int pp_numberorname = pp_number | pp_param_name pp_il = pyparsing.Word("il") -pp_choices = pp_param_name + pyparsing.Optional(pyparsing.OneOrMore("," + pp_param_name)) +pp_choices = pp_param_name + pyparsing.Optional( + pyparsing.OneOrMore("," + pp_param_name), +) pp_cont_param = ( pp_param_name @@ -99,7 +105,9 @@ + pp_param_name + "=" + pp_numberorname - + pyparsing.Optional(pyparsing.OneOrMore("," + pp_param_name + "=" + pp_numberorname)) + + pyparsing.Optional( + pyparsing.OneOrMore("," + pp_param_name + "=" + pp_numberorname), + ) + "}" ) @@ -123,8 +131,10 @@ def build_constant(param: Constant) -> str: return constant_template % (param.name, param.value, param.value) -def build_continuous(param: NormalIntegerHyperparameter | NormalFloatHyperparameter) -> str: - if type(param) in (NormalIntegerHyperparameter, NormalFloatHyperparameter): +def build_continuous( + param: NormalIntegerHyperparameter | NormalFloatHyperparameter, +) -> str: + if isinstance(param, HyperparameterWithPrior): param = param.to_uniform() float_template = "%s%s [%s, %s] [%s]" @@ -133,12 +143,18 @@ def build_continuous(param: NormalIntegerHyperparameter | NormalFloatHyperparame float_template += "l" int_template += "l" - q_prefix = "Q%d_" % (int(param.q),) if param.q is not None else "" + q_prefix = "" default_value = param.default_value if isinstance(param, IntegerHyperparameter): default_value = int(default_value) - return int_template % (q_prefix, param.name, param.lower, param.upper, default_value) + return int_template % ( + q_prefix, + param.name, + param.lower, + param.upper, + default_value, + ) return float_template % ( q_prefix, @@ -175,7 +191,11 @@ def build_condition(condition: ConditionComponent) -> str: ) if isinstance(condition, EqualsCondition): - return condition_template % (condition.child.name, condition.parent.name, condition.value) + return condition_template % ( + condition.child.name, + condition.parent.name, + condition.value, + ) raise NotImplementedError(condition) @@ -187,7 +207,7 @@ def build_forbidden(clause: AbstractForbiddenComponent) -> str: f"'{AbstractForbiddenComponent}', got '{type(clause)}'", ) - if not isinstance(clause, (ForbiddenEqualsClause, ForbiddenAndConjunction)): + if not isinstance(clause, ForbiddenEqualsClause | ForbiddenAndConjunction): raise NotImplementedError( "SMAC cannot handle '{}' of type {}".format(*str(clause)), (type(clause)), @@ -208,8 +228,7 @@ def build_forbidden(clause: AbstractForbiddenComponent) -> str: def read(pcs_string: Iterable[str]) -> ConfigurationSpace: - """ - Read in a :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` + """Read in a :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` definition from a pcs file. @@ -230,7 +249,7 @@ def read(pcs_string: Iterable[str]) -> ConfigurationSpace: pcs_string : Iterable[str] ConfigSpace definition in pcs format as an iterable of strings - Returns + Returns: ------- :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` The deserialized ConfigurationSpace object @@ -315,7 +334,11 @@ def read(pcs_string: Iterable[str]) -> ConfigurationSpace: name = param_list[0] choices = list(param_list[2:-4:2]) default_value = param_list[-2] - param = create["categorical"](name=name, choices=choices, default_value=default_value) + param = create["categorical"]( + name=name, + choices=choices, + default_value=default_value, + ) cat_ct += 1 except pyparsing.ParseException: pass @@ -341,7 +364,10 @@ def read(pcs_string: Iterable[str]) -> ConfigurationSpace: # TODO maybe add a check if the hyperparameter is # actually in the configuration space clause_list.append( - ForbiddenEqualsClause(configuration_space[tmp_list[0]], tmp_list[2]), + ForbiddenEqualsClause( + configuration_space[tmp_list[0]], + tmp_list[2], + ), ) else: raise NotImplementedError() @@ -386,8 +412,7 @@ def read(pcs_string: Iterable[str]) -> ConfigurationSpace: def write(configuration_space: ConfigurationSpace) -> str: - """ - Create a string representation of a + """Create a string representation of a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` in pcs format. This string can be written to file. @@ -403,10 +428,10 @@ def write(configuration_space: ConfigurationSpace) -> str: Parameters ---------- - configuration_space : :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` + configuration_space: a configuration space - Returns + Returns: ------- str The string representation of the configuration space @@ -456,9 +481,14 @@ def write(configuration_space: ConfigurationSpace) -> str: for dlc in dlcs: if isinstance(dlc, MultipleValueForbiddenClause): if not isinstance(dlc, ForbiddenInClause): - raise ValueError("SMAC cannot handle this forbidden " "clause: %s" % dlc) + raise ValueError( + "SMAC cannot handle this forbidden " "clause: %s" % dlc, + ) in_statements.append( - [ForbiddenEqualsClause(dlc.hyperparameter, value) for value in dlc.values], + [ + ForbiddenEqualsClause(dlc.hyperparameter, value) + for value in dlc.values + ], ) else: other_statements.append(dlc) diff --git a/ConfigSpace/read_and_write/pcs_new.py b/ConfigSpace/read_and_write/pcs_new.py index c22f2302..a4bba667 100644 --- a/ConfigSpace/read_and_write/pcs_new.py +++ b/ConfigSpace/read_and_write/pcs_new.py @@ -1,23 +1,23 @@ #!/usr/bin/env python -""" -PCS (parameter configuration space) is a simple, human-readable file format for the +"""PCS (parameter configuration space) is a simple, human-readable file format for the description of an algorithm's configurable parameters, their possible values, as well as any parameter dependencies. There exist an old and a new version. The new PCS format is part of the `Algorithm Configuration Library 2.0 `_. A detailed description of the **new** format can be found in the -`ACLIB 2.0 docs `_, in the -`SMACv2 docs `_ +`ACLIB 2.0 docs `_, +in the `SMACv2 docs `_ and further examples are provided in the `pysmac docs `_ .. note:: The PCS format definition has changed in the year 2016 and is supported by - AClib 2.0, as well as SMAC (v2 and v3). To write or to read the **old** version of pcs, - please use the :class:`~ConfigSpace.read_and_write.pcs` module. + AClib 2.0, as well as SMAC (v2 and v3). To write or to read the **old** version of + pcs, please use the :class:`~ConfigSpace.read_and_write.pcs` module. """ + from __future__ import annotations __authors__ = [ @@ -28,9 +28,9 @@ __contact__ = "automl.org" from collections import OrderedDict +from collections.abc import Iterable from io import StringIO from itertools import product -from typing import Iterable import pyparsing @@ -90,9 +90,13 @@ ) pp_param_operation = pyparsing.Word("in" + "!=" + "==" + ">" + "<") pp_digits = "0123456789" -pp_param_val = pp_param_name + pyparsing.Optional(pyparsing.OneOrMore("," + pp_param_name)) +pp_param_val = pp_param_name + pyparsing.Optional( + pyparsing.OneOrMore("," + pp_param_name), +) pp_plusorminus = pyparsing.Literal("+") | pyparsing.Literal("-") -pp_int = pyparsing.Combine(pyparsing.Optional(pp_plusorminus) + pyparsing.Word(pp_digits)) +pp_int = pyparsing.Combine( + pyparsing.Optional(pp_plusorminus) + pyparsing.Word(pp_digits), +) pp_float = pyparsing.Combine( pyparsing.Optional(pp_plusorminus) + pyparsing.Optional(pp_int) + "." + pp_int, ) @@ -112,9 +116,15 @@ # https://pythonhosted.org/pyparsing/pyparsing.Word-class.html pp_connectiveOR = pyparsing.Literal("||") pp_connectiveAND = pyparsing.Literal("&&") -pp_choices = pp_param_name + pyparsing.Optional(pyparsing.OneOrMore("," + pp_param_name)) -pp_sequence = pp_param_name + pyparsing.Optional(pyparsing.OneOrMore("," + pp_param_name)) -pp_ord_param = pp_param_name + pp_param_type + "{" + pp_sequence + "}" + "[" + pp_param_name + "]" +pp_choices = pp_param_name + pyparsing.Optional( + pyparsing.OneOrMore("," + pp_param_name), +) +pp_sequence = pp_param_name + pyparsing.Optional( + pyparsing.OneOrMore("," + pp_param_name), +) +pp_ord_param = ( + pp_param_name + pp_param_type + "{" + pp_sequence + "}" + "[" + pp_param_name + "]" +) pp_cont_param = ( pp_param_name + pp_param_type @@ -128,7 +138,9 @@ + "]" + pyparsing.Optional(pp_log) ) -pp_cat_param = pp_param_name + pp_param_type + "{" + pp_choices + "}" + "[" + pp_param_name + "]" +pp_cat_param = ( + pp_param_name + pp_param_type + "{" + pp_choices + "}" + "[" + pp_param_name + "]" +) pp_condition = ( pp_param_name + "|" @@ -153,7 +165,9 @@ + pp_param_name + "=" + pp_numberorname - + pyparsing.Optional(pyparsing.OneOrMore("," + pp_param_name + "=" + pp_numberorname)) + + pyparsing.Optional( + pyparsing.OneOrMore("," + pp_param_name + "=" + pp_numberorname), + ) + "}" ) @@ -186,7 +200,9 @@ def build_constant(param: Constant) -> str: return const_template % (param.name, param.value, param.value) -def build_continuous(param: NormalFloatHyperparameter | NormalIntegerHyperparameter) -> str: +def build_continuous( + param: NormalFloatHyperparameter | NormalIntegerHyperparameter, +) -> str: if type(param) in (NormalIntegerHyperparameter, NormalFloatHyperparameter): param = param.to_uniform() @@ -376,13 +392,11 @@ def condition_specification( def read(pcs_string: Iterable[str]) -> ConfigurationSpace: - """ - Read in a :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` + """Read in a :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` definition from a pcs file. - Example + Example: ------- - .. testsetup:: pcs_new_test from ConfigSpace import ConfigurationSpace @@ -404,7 +418,7 @@ def read(pcs_string: Iterable[str]) -> ConfigurationSpace: pcs_string : Iterable[str] ConfigSpace definition in pcs format - Returns + Returns: ------- :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` The deserialized ConfigurationSpace object @@ -551,9 +565,14 @@ def read(pcs_string: Iterable[str]) -> ConfigurationSpace: f" but its value is {forbidden_value}", ) - elif isinstance(hp, (CategoricalHyperparameter, OrdinalHyperparameter)): + elif isinstance( + hp, + CategoricalHyperparameter | OrdinalHyperparameter, + ): hp_values = ( - hp.choices if isinstance(hp, CategoricalHyperparameter) else hp.sequence + hp.choices + if isinstance(hp, CategoricalHyperparameter) + else hp.sequence ) forbidden_value_in_hp_values = tmp_list[2] in hp_values @@ -569,7 +588,10 @@ def read(pcs_string: Iterable[str]) -> ConfigurationSpace: raise ValueError("Unsupported Hyperparamter sorts") clause_list.append( - ForbiddenEqualsClause(configuration_space[tmp_list[0]], forbidden_value), + ForbiddenEqualsClause( + configuration_space[tmp_list[0]], + forbidden_value, + ), ) else: raise NotImplementedError() @@ -610,7 +632,9 @@ def read(pcs_string: Iterable[str]) -> ConfigurationSpace: ands = [] for and_part in condition: element_list = [ - element for _ in condition for element in and_part.split() + element + for _ in condition + for element in and_part.split() ] ands.append( condition_specification( @@ -665,12 +689,11 @@ def read(pcs_string: Iterable[str]) -> ConfigurationSpace: def write(configuration_space: ConfigurationSpace) -> str: - """ - Create a string representation of a + """Create a string representation of a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` in pcs_new format. This string can be written to file. - Example + Example: ------- .. doctest:: @@ -688,10 +711,10 @@ def write(configuration_space: ConfigurationSpace) -> str: Parameters ---------- - configuration_space : :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` - a configuration space + configuration_space: + A configuration space - Returns + Returns: ------- str The string representation of the configuration space @@ -732,7 +755,7 @@ def write(configuration_space: ConfigurationSpace) -> str: for condition in configuration_space.get_conditions(): if condition_lines.tell() > 0: condition_lines.write("\n") - if isinstance(condition, (AndConjunction, OrConjunction)): + if isinstance(condition, AndConjunction | OrConjunction): condition_lines.write(build_conjunction(condition)) else: condition_lines.write(build_condition(condition)) @@ -746,9 +769,14 @@ def write(configuration_space: ConfigurationSpace) -> str: for dlc in dlcs: if isinstance(dlc, MultipleValueForbiddenClause): if not isinstance(dlc, ForbiddenInClause): - raise ValueError("SMAC cannot handle this forbidden " "clause: %s" % dlc) + raise ValueError( + "SMAC cannot handle this forbidden " "clause: %s" % dlc, + ) in_statements.append( - [ForbiddenEqualsClause(dlc.hyperparameter, value) for value in dlc.values], + [ + ForbiddenEqualsClause(dlc.hyperparameter, value) + for value in dlc.values + ], ) else: other_statements.append(dlc) diff --git a/ConfigSpace/util.py b/ConfigSpace/util.py index 7455420a..81489c00 100644 --- a/ConfigSpace/util.py +++ b/ConfigSpace/util.py @@ -29,7 +29,8 @@ import copy from collections import deque -from typing import Any, Iterator, cast +from collections.abc import Iterator, Sequence +from typing import Any, cast import numpy as np @@ -69,7 +70,7 @@ def impute_inactive_values( If float, replace inactive parameters by the given float value, which should be able to be splitted apart by a tree-based model. - Returns + Returns: ------- :class:`~ConfigSpace.configuration_space.Configuration` A new configuration with the imputed values. @@ -105,8 +106,7 @@ def get_one_exchange_neighbourhood( num_neighbors: int = 4, stdev: float = 0.2, ) -> Iterator[Configuration]: - """ - Return all configurations in a one-exchange neighborhood. + """Return all configurations in a one-exchange neighborhood. The method is implemented as defined by: Frank Hutter, Holger H. Hoos and Kevin Leyton-Brown @@ -128,7 +128,7 @@ def get_one_exchange_neighbourhood( :class:`~ConfigSpace.hyperparameters.UniformFloatHyperparameter` and :class:`~ConfigSpace.hyperparameters.UniformIntegerHyperparameter`. - Returns + Returns: ------- Iterator It contains configurations, with values being situated around @@ -188,7 +188,9 @@ def get_one_exchange_neighbourhood( if number_of_sampled_neighbors >= 1: break if isinstance(hp, UniformFloatHyperparameter): - neighbor = hp.get_neighbors(value, random, number=1, std=stdev)[0] + neighbor = hp.get_neighbors(value, random, number=1, std=stdev)[ + 0 + ] else: neighbor = hp.get_neighbors(value, random, number=1)[0] else: @@ -256,8 +258,7 @@ def get_one_exchange_neighbourhood( def get_random_neighbor(configuration: Configuration, seed: int) -> Configuration: - """ - Draw a random neighbor by changing one parameter of a configuration. + """Draw a random neighbor by changing one parameter of a configuration. - If the parameter is categorical, it changes it to another value. - If the parameter is ordinal, it changes it to the next higher or @@ -276,7 +277,7 @@ def get_random_neighbor(configuration: Configuration, seed: int) -> Configuratio seed : int Used to generate a random state. - Returns + Returns: ------- :class:`~ConfigSpace.configuration_space.Configuration` The new neighbor @@ -295,7 +296,9 @@ def get_random_neighbor(configuration: Configuration, seed: int) -> Configuratio value = None while not active: iteration += 1 - rand_idx = random.randint(0, len(configuration)) if len(configuration) > 1 else 0 + rand_idx = ( + random.randint(0, len(configuration)) if len(configuration) > 1 else 0 + ) value = configuration.get_array()[rand_idx] if np.isfinite(value): @@ -315,7 +318,7 @@ def get_random_neighbor(configuration: Configuration, seed: int) -> Configuratio assert value is not None # Get a neighboor and adapt the rest of the configuration if necessary - neighbor = hp.get_neighbors(value, random, number=1, transform=True)[0] + neighbor = hp.to_value(hp.get_neighbors(value, rs=random, number=1))[0] previous_value = values[hp.name] values[hp.name] = neighbor @@ -334,8 +337,7 @@ def deactivate_inactive_hyperparameters( configuration_space: ConfigurationSpace, vector: None | np.ndarray = None, ) -> Configuration: - """ - Remove inactive hyperparameters from a given configuration. + """Remove inactive hyperparameters from a given configuration. Parameters ---------- @@ -352,7 +354,7 @@ def deactivate_inactive_hyperparameters( ``vector`` must be specified. If both are specified only ``configuration`` will be used. - Returns + Returns: ------- :class:`~ConfigSpace.configuration_space.Configuration` A configuration that is equivalent to the given configuration, except @@ -369,7 +371,9 @@ def deactivate_inactive_hyperparameters( hps: deque[Hyperparameter] = deque() - unconditional_hyperparameters = configuration_space.get_all_unconditional_hyperparameters() + unconditional_hyperparameters = ( + configuration_space.get_all_unconditional_hyperparameters() + ) hyperparameters_with_children = [] for uhp in unconditional_hyperparameters: children = configuration_space._children_of[uhp] @@ -419,8 +423,7 @@ def fix_types( configuration: dict[str, Any], configuration_space: ConfigurationSpace, ) -> dict[str, Any]: - """ - Iterate over all hyperparameters in the ConfigSpace + """Iterate over all hyperparameters in the ConfigSpace and fix the types of the parameter values in configuration. Parameters @@ -431,18 +434,18 @@ def fix_types( configuration_space : :class:`~ConfigSpace.configuration_space.ConfigurationSpace` Configuration space which knows the types for all parameter values - Returns + Returns: ------- dict configuration with fixed types of parameter values """ - def fix_type_from_candidates(value: Any, candidates: list[Any]) -> Any: + def fix_type_from_candidates(value: Any, candidates: Sequence[Any]) -> Any: result = [c for c in candidates if str(value) == str(c)] if len(result) != 1: raise ValueError( - f"Parameter value {value!s} cannot be matched to candidates {candidates}. " - "Either none or too many matching candidates.", + f"Parameter value {value} cannot be matched to candidates {candidates}." + " Either none or too many matching candidates.", ) return result[0] @@ -477,41 +480,40 @@ def generate_grid( configuration_space: ConfigurationSpace, num_steps_dict: dict[str, int] | None = None, ) -> list[Configuration]: - """ - Generates a grid of Configurations for a given ConfigurationSpace. + """Generates a grid of Configurations for a given ConfigurationSpace. Can be used, for example, for grid search. Parameters ---------- configuration_space: :class:`~ConfigSpace.configuration_space.ConfigurationSpace` - The Configuration space over which to create a grid of HyperParameter Configuration values. - It knows the types for all parameter values. + The Configuration space over which to create a grid of HyperParameter + Configuration values. It knows the types for all parameter values. num_steps_dict: dict - A dict containing the number of points to divide the grid side formed by Hyperparameters - which are either of type UniformFloatHyperparameter or type UniformIntegerHyperparameter. - The keys in the dict should be the names of the corresponding Hyperparameters and the values - should be the number of points to divide the grid side formed by the corresponding - Hyperparameter in to. + A dict containing the number of points to divide the grid side formed by + Hyperparameters which are either of type UniformFloatHyperparameter or + type UniformIntegerHyperparameter. The keys in the dict should be the names + of the corresponding Hyperparameters and the values should be the number of + points to divide the grid side formed by the corresponding Hyperparameter in to. - Returns + Returns: ------- list List containing Configurations. It is a cartesian product of tuples of HyperParameter values. Each tuple lists the possible values taken by the corresponding HyperParameter. - Within the cartesian product, in each element, the ordering of HyperParameters is the same - for the OrderedDict within the ConfigurationSpace. + Within the cartesian product, in each element, the ordering of HyperParameters + is the same for the OrderedDict within the ConfigurationSpace. """ def get_value_set(num_steps_dict: dict[str, int] | None, hp_name: str) -> tuple: - """ - Gets values along the grid for a particular hyperparameter. + """Gets values along the grid for a particular hyperparameter. - Uses the num_steps_dict to determine number of grid values for UniformFloatHyperparameter - and UniformIntegerHyperparameter. If these values are not present in num_steps_dict, the - quantization factor, q, of these classes will be used to divide the grid. NOTE: When q - is used if it is None, a ValueError is raised. + Uses the num_steps_dict to determine number of grid values for + UniformFloatHyperparameter and UniformIntegerHyperparameter. If these values + are not present in num_steps_dict, the quantization factor, q, of these + classes will be used to divide the grid. NOTE: When q is used if it + is None, a ValueError is raised. Parameters ---------- @@ -521,7 +523,7 @@ def get_value_set(num_steps_dict: dict[str, int] | None, hp_name: str) -> tuple: hp_name: str Hyperparameter name - Returns + Returns: ------- tuple Holds grid values for the given hyperparameter @@ -546,10 +548,6 @@ def get_value_set(num_steps_dict: dict[str, int] | None, hp_name: str) -> tuple: if num_steps_dict is not None and param.name in num_steps_dict: num_steps = num_steps_dict[param.name] grid_points = np.linspace(lower, upper, num_steps) - - # check for log and for rounding issues - elif param.q is not None: - grid_points = np.arange(lower, upper + param.q, param.q) else: raise ValueError( "num_steps_dict is None or doesn't contain the number of points" @@ -577,10 +575,6 @@ def get_value_set(num_steps_dict: dict[str, int] | None, hp_name: str) -> tuple: if num_steps_dict is not None and param.name in num_steps_dict: num_steps = num_steps_dict[param.name] grid_points = np.linspace(lower, upper, num_steps) - - # check for log and for rounding issues - elif param.q is not None: - grid_points = np.arange(lower, upper + param.q, param.q) else: raise ValueError( "num_steps_dict is None or doesn't contain the number of points " @@ -602,15 +596,18 @@ def get_value_set(num_steps_dict: dict[str, int] | None, hp_name: str) -> tuple: raise TypeError(f"Unknown hyperparameter type {type(param)}") - def get_cartesian_product(value_sets: list[tuple], hp_names: list[str]) -> list[dict[str, Any]]: - """ - Returns a grid for a subspace of the configuration with given hyperparameters + def get_cartesian_product( + value_sets: list[tuple], + hp_names: list[str], + ) -> list[dict[str, Any]]: + """Returns a grid for a subspace of the configuration with given hyperparameters and their grid values. Takes a list of tuples of grid values of the hyperparameters and list of - hyperparameter names. The outer list iterates over the hyperparameters corresponding - to the order in the list of hyperparameter names. - The inner tuples contain grid values of the hyperparameters for each hyperparameter. + hyperparameter names. The outer list iterates over the hyperparameters + corresponding to the order in the list of hyperparameter names. + The inner tuples contain grid values of the hyperparameters for each + hyperparameter. Parameters ---------- @@ -620,7 +617,7 @@ def get_cartesian_product(value_sets: list[tuple], hp_names: list[str]) -> list[ hp_names: list of strs List of hyperparameter names - Returns + Returns: ------- list of dicts List of configuration dicts @@ -633,12 +630,12 @@ def get_cartesian_product(value_sets: list[tuple], hp_names: list[str]) -> list[ grid = [] for element in itertools.product(*value_sets): - config_dict = dict(zip(hp_names, element)) + config_dict = dict(zip(hp_names, element, strict=False)) grid.append(config_dict) return grid - # list of tuples: each tuple within is the grid values to be taken on by a Hyperparameter + # Each tuple within is the grid values to be taken on by a Hyperparameter value_sets = [] hp_names = [] @@ -649,16 +646,19 @@ def get_cartesian_product(value_sets: list[tuple], hp_names: list[str]) -> list[ hp_names.append(hp_name) # Create a Cartesian product of above allowed values for the HPs. Hold them in an - # "unchecked" deque because some of the conditionally dependent HPs may become active - # for some of the elements of the Cartesian product and in these cases creating a - # Configuration would throw an Error (see below). + # "unchecked" deque because some of the conditionally dependent HPs may become + # active for some of the elements of the Cartesian product and in these cases + # creating a Configuration would throw an Error (see below). # Creates a deque of Configuration dicts unchecked_grid_pts = deque(get_cartesian_product(value_sets, hp_names)) checked_grid_pts = [] while len(unchecked_grid_pts) > 0: try: - grid_point = Configuration(configuration_space, values=unchecked_grid_pts[0]) + grid_point = Configuration( + configuration_space, + values=unchecked_grid_pts[0], + ) checked_grid_pts.append(grid_point) # When creating a configuration that violates a forbidden clause, simply skip it @@ -683,7 +683,9 @@ def get_cartesian_product(value_sets: list[tuple], hp_names: list[str]) -> list[ and new_hp_name not in unchecked_grid_pts[0] ): all_cond_ = True - for cond in configuration_space._parent_conditions_of[new_hp_name]: + for cond in configuration_space._parent_conditions_of[ + new_hp_name + ]: if not cond.evaluate(unchecked_grid_pts[0]): all_cond_ = False if all_cond_: @@ -697,9 +699,10 @@ def get_cartesian_product(value_sets: list[tuple], hp_names: list[str]) -> list[ # active HP when in this except block? if len(new_active_hp_names) <= 0: raise RuntimeError( - "Unexpected error: There should have been a newly activated hyperparameter" - f" for the current configuration values: {unchecked_grid_pts[0]!s}. " - "Please contact the developers with the code you ran and the stack trace.", + "Unexpected error: There should have been a newly activated" + " hyperparameter for the current configuration values:" + f" {unchecked_grid_pts[0]!s}. Please contact the developers with" + " the code you ran and the stack trace.", ) from None new_conditonal_grid = get_cartesian_product(value_sets, hp_names) diff --git a/pyproject.toml b/pyproject.toml index dd1b26fa..a5ca8446 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,22 +66,11 @@ dependencies = [ ] [project.optional-dependencies] -dev = [ - "mypy", - "pre-commit", - "build", - "ruff", - "black", -] +dev = ["mypy", "pre-commit", "build", "ruff"] -test = [ - "pytest>=4.6", - "pytest-cov", -] +test = ["pytest>=4.6", "pytest-cov"] -docs = [ - "automl_sphinx_theme>=0.1.11", -] +docs = ["automl_sphinx_theme>=0.1.11"] [build-system] requires = ["setuptools", "wheel", "oldest-supported-numpy", "Cython==0.29.36"] @@ -112,19 +101,17 @@ exclude_lines = [ "if TYPE_CHECKING", ] -[tool.black] -target-version = ['py38'] -line-length = 100 - # https://github.com/charliermarsh/ruff [tool.ruff] -target-version = "py38" -line-length = 100 -show-source = true +target-version = "py310" +line-length = 88 +output-format = "full" src = ["ConfigSpace", "test"] +[tool.ruff.lint] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +extend-safe-fixes = ["ALL"] select = [ "A", @@ -195,8 +182,8 @@ ignore = [ "PLR0912", # Too many branches "PLR0913", # Too many arguments to function call "PLR2004", # Magic constants - "N999", # Invalid Module name - "N802", # Function name should be lowercase + "N999", # Invalid Module name + "N802", # Function name should be lowercase # These tend to be lighweight and confuse pyright ] @@ -222,11 +209,11 @@ exclude = [ "venv", "docs", # This is vendored, ignore it - "ConfigSpace/nx/**" + "ConfigSpace/nx/**", ] # Exclude a variety of commonly ignored directories. -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "test/*.py" = [ "S101", "D102", @@ -247,22 +234,12 @@ exclude = [ "PLR0915", "BLE001", ] -"setup.py" = ["D102"] "__init__.py" = ["I002"] -"ConfigSpace/read_and_write/pcs_new.py" = [ - "N816", - "D103", - "PLW2901", -] -"ConfigSpace/read_and_write/pcs.py" = [ - "N816", - "D103", - "PLW2901", - "T201", -] +"ConfigSpace/read_and_write/pcs_new.py" = ["N816", "D103", "PLW2901"] +"ConfigSpace/read_and_write/pcs.py" = ["N816", "D103", "PLW2901", "T201"] -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["ConfigSpace"] no-lines-before = ["future"] required-imports = ["from __future__ import annotations"] @@ -271,11 +248,13 @@ extra-standard-library = ["typing_extensions"] force-wrap-aliases = true [tool.ruff.pydocstyle] -convention = "numpy" +convention = "google" +[tool.ruff.lint.pylint] +max-args = 10 # Changed from default of 5 [tool.mypy] -python_version = "3.8" +python_version = "3.10" packages = ["ConfigSpace", "test"] show_error_codes = true @@ -285,8 +264,8 @@ warn_unused_configs = true # warn about unused [tool.mypy] lines follow_imports = "normal" # Type check top level api code we use from imports ignore_missing_imports = false # prefer explicit ignores -disallow_untyped_defs = true # All functions must have types -disallow_incomplete_defs = true # ...all types +disallow_untyped_defs = true # All functions must have types +disallow_incomplete_defs = true # ...all types disallow_untyped_decorators = false # ... but not decorators no_implicit_optional = true @@ -310,5 +289,5 @@ disallow_untyped_decorators = false # Test decorators are not properly typed disallow_incomplete_defs = false # Sometimes we just want to ignore verbose types [[tool.mypy.overrides]] -module = ["ConfigSpace.nx.*"] # This is vendored, we ignore it +module = ["ConfigSpace.nx.*"] # This is vendored, we ignore it ignore_errors = true diff --git a/setup.py b/setup.py index d2360998..e69de29b 100644 --- a/setup.py +++ b/setup.py @@ -1,121 +0,0 @@ -"""Setup.py for ConfigSpace. - -# Profiling -Set the below flag to True to enable profiling of the code. This will cause some minor performance -overhead so it should only be used for debugging purposes. - -Use [`py-spy`](https://github.com/benfred/py-spy) with [speedscope.app](https://www.speedscope.app/) -```bash -pip install py-spy -py-spy record --rate 800 --format speedscope --subprocesses --native -o profile.svg -- python