from __future__ import annotations from dataclasses import dataclass import json from typing import TYPE_CHECKING from typing import Union import warnings import optuna from optuna.version import __version__ as optuna_ver from packaging import version if TYPE_CHECKING: from typing import Any from typing import Literal from typing import Optional from typing import Sequence from typing import TypedDict ChoiceWidgetJSON = TypedDict( "ChoiceWidgetJSON", { "type": Literal["choice"], "description": Optional[str], "choices": list[str], "values": list[float], "user_attr_key": Optional[str], }, ) SliderWidgetLabel = TypedDict( "SliderWidgetLabel", {"value": float, "label": str}, ) SliderWidgetJSON = TypedDict( "SliderWidgetJSON", { "type": Literal["slider"], "description": Optional[str], "min": float, "max": float, "step": Optional[float], "labels": Optional[list[SliderWidgetLabel]], "user_attr_key": Optional[str], }, ) TextInputWidgetJSON = TypedDict( "TextInputWidgetJSON", { "type": Literal["text"], "description": Optional[str], "user_attr_key": Optional[str], "optional": bool, }, ) UserAttrRefJSON = TypedDict("UserAttrRefJSON", {"type": Literal["user_attr"], "key": str}) FormWidgetJSON = TypedDict( "FormWidgetJSON", { "output_type": Literal["objective", "user_attr"], "widgets": Sequence[ Union[ChoiceWidgetJSON, SliderWidgetJSON, TextInputWidgetJSON, UserAttrRefJSON] ], }, ) @dataclass class ChoiceWidget: """A widget representing a choice with associated values. Args: choices: A list of strings representing the available choices. values: A list of float values associated with each choice. description: A description of the widget. Defaults to None. user_attr_key: The key used by `register_user_attr_form_widgets`. Form output is saved as `trial.user_attrs[user_attr_key]`. Defaults to None. Example: .. code-block:: python from optuna_dashboard import ChoiceWidget choice_widget = ChoiceWidget( choices=["A", "B", "C"], values=[1.0, 2.0, 3.0], description="Choose one" ) """ choices: list[str] values: list[float] description: Optional[str] = None user_attr_key: Optional[str] = None def to_dict(self) -> ChoiceWidgetJSON: """Convert the ChoiceWidget object to a dictionary. Returns: ChoiceWidgetJSON: A dictionary representing the ChoiceWidget object. """ return { "type": "choice", "description": self.description, "choices": self.choices, "values": self.values, "user_attr_key": self.user_attr_key, } @classmethod def _from_dict(cls, d: dict[str, Any]) -> ChoiceWidget: assert d.get("type") == "choice" return cls( description=d.get("description"), choices=d["choices"], values=d["values"], user_attr_key=d.get("user_attr_key"), ) @dataclass class SliderWidget: """A widget representing a slider for selecting a value within a range. Args: min: The minimum value of the slider. max: The maximum value of the slider. step: The step size for the slider. Defaults to None. labels: A list of tuples containing value and label for the slider. Defaults to None. description: A description for the slider. Defaults to None. user_attr_key: The key used by `register_user_attr_form_widgets`. Form output is saved as `trial.user_attrs[user_attr_key]`. Defaults to None. Example: .. code-block:: python from optuna_dashboard import SliderWidget slide_widget = SliderWidget(min=0, max=10, step=1, description="Example slider") """ min: float max: float step: Optional[float] = None labels: Optional[list[tuple[float, str]]] = None description: Optional[str] = None user_attr_key: Optional[str] = None def to_dict(self) -> SliderWidgetJSON: """Convert the SliderWidget instance to a dictionary. Returns: SliderWidgetJSON: A dictionary representation of the SliderWidget instance. """ labels: Optional[list[SliderWidgetLabel]] = None if self.labels is not None: labels = [{"value": value, "label": label} for value, label in self.labels] return { "type": "slider", "description": self.description, "min": self.min, "max": self.max, "step": self.step, "labels": labels, "user_attr_key": self.user_attr_key, } @classmethod def _from_dict(cls, d: dict[str, Any]) -> SliderWidget: assert d.get("type") == "slider" labels = d.get("labels") if labels is not None: labels = [(label["value"], label["label"]) for label in labels] return cls( description=d.get("description"), min=d["min"], max=d["max"], step=d.get("step"), labels=labels, user_attr_key=d.get("user_attr_key"), ) @dataclass class TextInputWidget: """ A text input widget class that defines a text input field. Args: description: A description of the text input field. user_attr_key: The key used by `register_user_attr_form_widgets`. Form output is saved as `trial.user_attrs[user_attr_key]`. Defaults to None. optional: If True, an empty string is acceptable. Example: .. code-block:: python from optuna_dashboard import TextInputWidget text_input = TextInputWidget(description="Text Input Example") """ description: Optional[str] = None user_attr_key: Optional[str] = None optional: bool = False def to_dict(self) -> TextInputWidgetJSON: """ Converts the TextInputWidget instance to a dictionary representation. Returns: TextInputWidgetJSON: The dictionary representation of the TextInputWidget instance. """ return { "type": "text", "description": self.description, "user_attr_key": self.user_attr_key, "optional": self.optional, } @classmethod def _from_dict(cls, d: dict[str, Any]) -> TextInputWidget: assert d.get("type") == "text" return cls( description=d.get("description"), user_attr_key=d.get("user_attr_key"), optional=d.get("optional", False), ) @dataclass class ObjectiveUserAttrRef: """ A class representing a reference to a value of `trial.user_attrs`. When combined with `register_objective_form_widgets`, users can tell values that are registered to `trial.user_attrs` during the human-in-the-loop optimization. Args: key: The key of `trial.user_attrs` being referenced. Example: .. code-block:: python from optuna_dashboard import ObjectiveUserAttrRef user_attr_ref = ObjectiveUserAttrRef(key="key") """ key: str def to_dict(self) -> UserAttrRefJSON: """ Converts the ObjectiveUserAttrRef instance to a dictionary representation. Returns: UserAttrRefJSON: The dictionary representation of the ObjectiveUserAttrRef instance. """ return { "type": "user_attr", "key": self.key, } @classmethod def _from_dict(cls, d: dict[str, Any]) -> ObjectiveUserAttrRef: assert d.get("type") == "user_attr" return cls( key=d["key"], ) ObjectiveFormWidget = Union[ChoiceWidget, SliderWidget, TextInputWidget, ObjectiveUserAttrRef] # For backward compatibility. ObjectiveChoiceWidget = ChoiceWidget ObjectiveSliderWidget = SliderWidget ObjectiveTextInputWidget = TextInputWidget FORM_WIDGETS_KEY = "dashboard:form_widgets:v2" def dict_to_form_widget(d: dict[str, Any]) -> ObjectiveFormWidget: """Restore form widget objects from the dictionary. Args: d: A dictionary object. Returns: object: an instance of the restored form widget class. """ widget_type = d.get("type", None) if widget_type == "choice": return ChoiceWidget._from_dict(d) elif widget_type == "slider": return SliderWidget._from_dict(d) elif widget_type == "text": return TextInputWidget._from_dict(d) elif widget_type == "user_attr": return ObjectiveUserAttrRef._from_dict(d) raise ValueError("Unexpected widget type") def register_objective_form_widgets( study: optuna.Study, widgets: list[ObjectiveFormWidget] ) -> None: """ Register a list of form widgets to an Optuna study. Submitted values to the forms are told as each trial's objective values. Args: study: The Optuna study object to register the form widgets for. widgets: A list of ObjectiveFormWidget objects to be registered in the study. Raises: ValueError: If the length of study directions is not equal to the length of widgets. Warning: If any widget has `user_attr_key` specified, but it will not be used. Examples: .. code-block:: python import optuna from optuna_dashboard import ChoiceWidget, SliderWidget from optuna_dashboard import register_objective_form_widgets study = optuna.create_study() register_objective_form_widgets( study, widgets=[ ObjectiveChoiceWidget( choices=["Good 👍", "Bad 👎"], values=[-1, 1], description="Please input your score!", ), ObjectiveSliderWidget( min=1, max=10, step=1, description="Higher is better.", ), ], ) """ if version.parse(optuna_ver) < version.Version("3.2") and isinstance( study._storage, optuna.storages._CachedStorage ): warnings.warn( "Human-in-the-loop optimization will not work with _CachedStorage in Optuna prior" " to v3.2. See https://optuna-dashboard.readthedocs.io/en/latest/errors.html" " for details." ) if len(study.directions) != len(widgets): raise ValueError("The length of actions must be the same with the number of objectives.") if any( not isinstance(w, ObjectiveUserAttrRef) and w.user_attr_key is not None for w in widgets ): warnings.warn("`user_attr_key` specified, but it will not be used.") if any(isinstance(w, TextInputWidget) and w.optional for w in widgets): raise ValueError("TextInputWidget.optional must be False.") form_widgets: FormWidgetJSON = { "output_type": "objective", "widgets": [w.to_dict() for w in widgets], } study._storage.set_study_system_attr( study._study_id, FORM_WIDGETS_KEY, # TODO(c-bata): Remove type: ignore comment form_widgets, # type: ignore ) def register_user_attr_form_widgets( study: optuna.Study, widgets: list[ObjectiveFormWidget] ) -> None: """ Register a list of form widgets to an Optuna study. Submitted values to the forms are registered as each trial's user_attrs. Args: study: The Optuna study object to register the form widgets for. widgets: A list of ObjectiveFormWidget objects to be registered in the study. Raises: ValueError: If an ObjectiveUserAttrRef is specified or if `user_attr_key` is not specified. ValueError: If `user_attr_key` is not unique for each widget. Examples: .. code-block:: python import optuna from optuna_dashboard import ChoiceWidget, SliderWidget from optuna_dashboard import register_user_attr_form_widgets study = optuna.create_study() register_user_attr_form_widgets( study, widgets=[ ChoiceWidget( choices=["Good 👍", "Bad 👎"], values=[-1, 1], description="Please input your score!", user_attr_key="hitl/choice", ), SliderWidget( min=1, max=10, step=1, description="Higher is better.", user_attr_key="hitl/slider", ), ], ) """ if version.parse(optuna_ver) < version.Version("3.2") and isinstance( study._storage, optuna.storages._CachedStorage ): warnings.warn( "Human-in-the-loop optimization will not work with _CachedStorage in Optuna prior" " to v3.2. See https://optuna-dashboard.readthedocs.io/en/latest/errors.html" " for details." ) user_attr_keys = set() widget_dicts: list[Union[ChoiceWidgetJSON, SliderWidgetJSON, TextInputWidgetJSON]] = [] for w in widgets: if isinstance(w, ObjectiveUserAttrRef): raise ValueError("ObjectiveUserAttrRef can't be specified.") if w.user_attr_key is None: raise ValueError("`user_attr_key` is not specified.") user_attr_keys.add(w.user_attr_key) widget_dicts.append(w.to_dict()) if len(widget_dicts) != len(user_attr_keys): raise ValueError("`user_attr_key` must be unique for each widget.") form_widgets: FormWidgetJSON = { "output_type": "user_attr", "widgets": widget_dicts, } study._storage.set_study_system_attr( study._study_id, FORM_WIDGETS_KEY, # TODO(c-bata): Remove type: ignore comment form_widgets, # type: ignore ) def get_form_widgets_json(study_system_attr: dict[str, Any]) -> Optional[FormWidgetJSON]: if FORM_WIDGETS_KEY in study_system_attr: return study_system_attr[FORM_WIDGETS_KEY] # For optuna-dashboard v0.9.0 and v0.9.0b6 users if "dashboard:objective_form_widgets:v1" in study_system_attr: return { "output_type": "objective", "widgets": study_system_attr["dashboard:objective_form_widgets:v1"], } # For optuna-dashboard v0.9.0b5 users if "dashboard:objective_form_widgets" in study_system_attr: return { "output_type": "objective", "widgets": json.loads(study_system_attr["dashboard:objective_form_widgets"]), } return None