diff --git a/src/ConfigSpace/api/types/categorical.py b/src/ConfigSpace/api/types/categorical.py index 1273f402..f69ff82b 100644 --- a/src/ConfigSpace/api/types/categorical.py +++ b/src/ConfigSpace/api/types/categorical.py @@ -1,14 +1,13 @@ from __future__ import annotations from collections.abc import Sequence -from typing import Literal, Union, overload -from typing_extensions import TypeAlias +from typing import Literal, TypeVar, overload from ConfigSpace.hyperparameters import CategoricalHyperparameter, OrdinalHyperparameter from ConfigSpace.types import NotSet, _NotSet # We only accept these types in `items` -T: TypeAlias = Union[str, int, float] +T = TypeVar("T") # ordered False -> CategoricalHyperparameter diff --git a/src/ConfigSpace/hyperparameters/categorical.py b/src/ConfigSpace/hyperparameters/categorical.py index d697f137..4d54a2b3 100644 --- a/src/ConfigSpace/hyperparameters/categorical.py +++ b/src/ConfigSpace/hyperparameters/categorical.py @@ -252,10 +252,13 @@ def __init__( # This can fail with a ValueError if the choices contain arbitrary objects # that are list like. seq_choices = np.asarray(choices) + if seq_choices.ndim != 1: + raise ValueError # NOTE: Unfortunatly, numpy will promote number types to str # if there are string types in the array, where we'd rather # stick to object type in that case. Hence the manual... + print(seq_choices) if seq_choices.dtype.kind in {"U", "S"} and not all( isinstance(choice, str) for choice in choices ): diff --git a/src/ConfigSpace/hyperparameters/hp_components.py b/src/ConfigSpace/hyperparameters/hp_components.py index cc3d6820..af2fe683 100644 --- a/src/ConfigSpace/hyperparameters/hp_components.py +++ b/src/ConfigSpace/hyperparameters/hp_components.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Sequence from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Protocol, TypeVar from typing_extensions import override @@ -202,6 +203,8 @@ def to_vector(self, value: Array[Any]) -> Array[f64]: @override def legal_value(self, value: Array[Any]) -> Mask: + print(value) + print(value, self.seq) if self._lookup is not None: return np.array([v in self._lookup for v in value], dtype=np.bool_) @@ -513,6 +516,12 @@ def to_vector(self, value: ObjectArray) -> Array[f64]: @override def to_value(self, vector: Array[f64]) -> ObjectArray: + if isinstance(self.value, Sequence) and not isinstance(self.value, str): + # We have to convert it into a numpy array of objects carefully + # https://stackoverflow.com/a/47389566/5332072 + _v = np.empty(len(vector), dtype=object) + _v[:] = [self.value] * len(vector) + return _v return np.full_like(vector, self.value, dtype=object) @override diff --git a/src/ConfigSpace/hyperparameters/hyperparameter.py b/src/ConfigSpace/hyperparameters/hyperparameter.py index c7896917..9fb12f94 100644 --- a/src/ConfigSpace/hyperparameters/hyperparameter.py +++ b/src/ConfigSpace/hyperparameters/hyperparameter.py @@ -126,7 +126,7 @@ def __init__( self._vector_dist = vector_dist self._transformer = transformer self._neighborhood = neighborhood - self._neighborhood_size = neighborhood_size + self._neighborhood_size = neighborhood_size # type: ignore self._value_cast = value_cast if not self.legal_value(self.default_value): diff --git a/src/ConfigSpace/hyperparameters/ordinal.py b/src/ConfigSpace/hyperparameters/ordinal.py index 1c9aad31..b0609258 100644 --- a/src/ConfigSpace/hyperparameters/ordinal.py +++ b/src/ConfigSpace/hyperparameters/ordinal.py @@ -94,6 +94,8 @@ def __init__( # This can fail with a ValueError if the choices contain arbitrary objects # that are list like. seq_choices = np.asarray(sequence) + if seq_choices.ndim != 1: + raise ValueError # NOTE: Unfortunatly, numpy will promote number types to str # if there are string types in the array, where we'd rather @@ -228,7 +230,7 @@ def pdf_values(self, values: Sequence[Any] | Array[Any]) -> Array[f64]: if values.ndim != 1: raise ValueError("Method pdf expects a one-dimensional numpy array") - vector = self.to_vector(values) + vector = self.to_vector(values) # type: ignore return self.pdf_vector(vector) if self._contains_sequence_as_value: diff --git a/test/test_hyperparameters.py b/test/test_hyperparameters.py index 3236dcd9..3c239ee2 100644 --- a/test/test_hyperparameters.py +++ b/test/test_hyperparameters.py @@ -99,6 +99,7 @@ def test_constant(): # Test getting the size for constant in (c1, c2, c3, c4, c5, c1_meta, c6, c7, c8): assert constant.size == 1 + _ = str(constant) # Ensure str repr works with pytest.raises(ValueError): _ = Constant("value", np.array([1, 2])) @@ -268,6 +269,7 @@ def test_uniformfloat(): # Test get_size for float_hp in (f1, f3, f4, f5): assert np.isinf(float_hp.size) + _ = str(float_hp) # Ensure str repr works def test_uniformfloat_to_integer(): @@ -575,6 +577,7 @@ def test_normalfloat(): # Test get_size for float_hp in (f1, f2, f6): assert np.isinf(float_hp.size) + _ = str(float_hp) # Ensure str repr works with pytest.raises(ValueError): _ = NormalFloatHyperparameter( @@ -2147,6 +2150,11 @@ def test_categorical(): assert f5.size == 1000 assert f6.size == 2 + fn = CategoricalHyperparameter("param", [("a", "b"), [0, 1]]) + assert fn.default_value == ("a", "b") + assert fn.legal_value(("a", "b")) + _ = str(fn) + def test_cat_equal(): # Test that weights are properly normalized and compared @@ -2756,6 +2764,14 @@ def test_ordinal_is_legal(): assert not f1.legal_vector("Hahaha") # type: ignore +def test_ordinal_nested_lists_prints_correctly(): + f1 = OrdinalHyperparameter( + "temp", + [["freezing", "cold", "warm", "hot"], ["a", "b"]], + ) + _ = str(f1) + + def test_ordinal_check_order(): f1 = OrdinalHyperparameter("temp", ["freezing", "cold", "warm", "hot"]) assert f1.check_order("freezing", "cold")