diff --git a/optuna_dashboard/_cached_extra_study_property.py b/optuna_dashboard/_cached_extra_study_property.py index 2f27fa64..24e22372 100644 --- a/optuna_dashboard/_cached_extra_study_property.py +++ b/optuna_dashboard/_cached_extra_study_property.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +import numbers import threading from typing import List from typing import Optional @@ -85,9 +86,8 @@ class _CachedExtraStudyProperty: self._cursor = next_cursor def _update_user_attrs(self, trial: FrozenTrial) -> None: - # TODO(c-bata): Support numpy-specific number types. current_user_attrs = { - k: not isinstance(v, bool) and isinstance(v, (int, float)) + k: not isinstance(v, bool) and isinstance(v, numbers.Real) for k, v in trial.user_attrs.items() } for attr_name, current_is_sortable in current_user_attrs.items(): diff --git a/optuna_dashboard/_serializer.py b/optuna_dashboard/_serializer.py index b5a3b305..7030abec 100644 --- a/optuna_dashboard/_serializer.py +++ b/optuna_dashboard/_serializer.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import datetime import json +import numbers from typing import Any from typing import TYPE_CHECKING from typing import Union @@ -104,6 +105,8 @@ def serialize_attrs(attrs: dict[str, Any]) -> list[Attribute]: value = "" elif isinstance(v, str): value = v + elif isinstance(v, numbers.Real): + value = str(v) else: value = json.dumps(v) value = value[:MAX_ATTR_LENGTH] if len(value) > MAX_ATTR_LENGTH else value diff --git a/python_tests/test_cached_extra_study_property.py b/python_tests/test_cached_extra_study_property.py index c02006a7..dcd5bc5e 100644 --- a/python_tests/test_cached_extra_study_property.py +++ b/python_tests/test_cached_extra_study_property.py @@ -4,6 +4,7 @@ from typing import Any from unittest import TestCase import warnings +import numpy as np import optuna from optuna import create_trial from optuna.distributions import BaseDistribution @@ -254,11 +255,29 @@ class _CachedExtraStudyPropertyUserAttrs(TestCase): def test_infer_sortable(self) -> None: user_attrs_list: list[dict[str, Any]] = [ - {"a": 1, "b": 1, "c": 1, "d": "a", "e": 1, "f": True}, + { + "a": 1, + "b": 1, + "c": 1, + "d": "a", + "e": 1, + "f": True, + "g": np.float128(1.1), + "h": np.int64(2), + }, {"a": 2, "b": "a", "c": "a", "d": "a"}, {"a": 3, "b": None, "c": 3, "d": "a", "e": 3}, ] - expected = {"a": True, "b": False, "c": False, "d": False, "e": True, "f": False} + expected = { + "a": True, + "b": False, + "c": False, + "d": False, + "e": True, + "f": False, + "g": True, + "h": True, + } trials = [] for user_attrs in user_attrs_list: diff --git a/python_tests/test_serializers.py b/python_tests/test_serializers.py index 0d1546d2..d1bdf59b 100644 --- a/python_tests/test_serializers.py +++ b/python_tests/test_serializers.py @@ -2,6 +2,7 @@ from __future__ import annotations import sys +import numpy as np import optuna from optuna_dashboard._serializer import serialize_attrs from optuna_dashboard._serializer import serialize_study_detail @@ -25,6 +26,32 @@ def test_serialize_dict() -> None: assert len(serialized) <= 1 +def test_serialize_numpy_integer() -> None: + serialized = serialize_attrs( + { + "int8": np.int8(1), + "int16": np.int16(1), + "int32": np.int32(1), + "int64": np.int64(1), + } + ) + assert len(serialized) == 4 + assert all([v["value"] == "1" for v in serialized]) + + +def test_serialize_numpy_floating() -> None: + serialized = serialize_attrs( + { + "float16": np.float16(1.0), + "float32": np.float32(1.0), + "float64": np.float64(1.0), + "float128": np.float128(1.0), + } + ) + assert len(serialized) == 4 + assert all([v["value"] == "1.0" for v in serialized]) + + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_get_study_detail_is_preferential() -> None: storage = optuna.storages.InMemoryStorage()