From 29cfc4730ebf8bf2315b7102e084097b9a124e2a Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 23 Jun 2023 16:53:17 +0900 Subject: [PATCH 01/39] Add streamlit helper --- optuna_dashboard/_streamlit_helper.py | 82 +++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 optuna_dashboard/_streamlit_helper.py diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py new file mode 100644 index 00000000..11ccfb3b --- /dev/null +++ b/optuna_dashboard/_streamlit_helper.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import optuna +from optuna.trial import FrozenTrial +import streamlit as st + +from ._form_widget import get_form_widgets_json +from ._note import get_note_from_system_attrs + + +def render_trial_note(study: optuna.Study, trial: FrozenTrial) -> None: + """Write a trial note to UI as a markdown format. + + Args: + study: The optuna study object. + trial: The optuna trial object to get note. + """ + note = get_note_from_system_attrs(study.system_attrs, trial._trial_id) + st.markdown(note["body"], unsafe_allow_html=True) + + +def render_widgets(study: optuna.Study, trial: FrozenTrial) -> None: + """Render user input widgets to UI. + + Args: + study: The optuna study object to get widget specification. + trial: The optuna trial object to tell user feedbacks. + """ + + form_widgets_dict = get_form_widgets_json(study.system_attrs) + if form_widgets_dict is None: + return + + st.write("## Objective Form Widgets") + + widgets = form_widgets_dict["widgets"] + values = [] + with st.form("user_input", clear_on_submit=False): + for widget in widgets: + if widget["type"] == "choice": + value = st.radio( + widget["description"], + widget["values"], + format_func=lambda choice: widget["choices"][ + widget["values"].index(choice) + ], # NOQA + horizontal=True, + ) + values.append(value) + elif widget["type"] == "slider": + # NOTE: It is difficult to reflect labels + value = st.slider( + widget["description"], + min_value=widget["min"], + max_value=widget["max"], + step=widget["step"], + ) + values.append(value) + elif widget["type"] == "text": + # TODO (kaitos): Consider optional + value = st.text_input(widget["description"]) + values.append(value) + elif widget["type"] == "user_attr": + value = trial.user_attrs[widget["key"]] + values.append(value) + else: + raise ValueError("Unsupported widget type.") + submitted = st.form_submit_button("Submit") + + storage = study._storage + output_type = form_widgets_dict["output_type"] + if submitted: + if output_type == "objective": + study.tell(trial.number, values) + elif output_type == "user_attr": + for widget, value in zip(widgets, values): + storage.set_trial_user_attr( + trial._trial_id, key=widget["user_attr_key"], value=value + ) + else: + st.warning("Detect unsupported output_type") + st.success("Feedback submitted!") From 53d4ae74542696c26e9611ac056fb0fb7595f232 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 23 Jun 2023 17:16:46 +0900 Subject: [PATCH 02/39] update comment --- optuna_dashboard/_streamlit_helper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index 11ccfb3b..a62a0a01 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -43,12 +43,12 @@ def render_widgets(study: optuna.Study, trial: FrozenTrial) -> None: widget["values"], format_func=lambda choice: widget["choices"][ widget["values"].index(choice) - ], # NOQA + ], # noqa: B023 horizontal=True, ) values.append(value) elif widget["type"] == "slider": - # NOTE: It is difficult to reflect labels + # NOTE: It is difficult to reflect "labels. value = st.slider( widget["description"], min_value=widget["min"], @@ -57,7 +57,7 @@ def render_widgets(study: optuna.Study, trial: FrozenTrial) -> None: ) values.append(value) elif widget["type"] == "text": - # TODO (kaitos): Consider optional + # TODO (kaitos): It is better to consider "optional". value = st.text_input(widget["description"]) values.append(value) elif widget["type"] == "user_attr": From e3821d37a3cf901018d8687810f7ac56aca469b1 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 23 Jun 2023 17:48:08 +0900 Subject: [PATCH 03/39] Add render_user_attr_form_widgets --- optuna_dashboard/_streamlit_helper.py | 42 +++++++++++++++------------ 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index a62a0a01..2867673c 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -9,7 +9,7 @@ from ._note import get_note_from_system_attrs def render_trial_note(study: optuna.Study, trial: FrozenTrial) -> None: - """Write a trial note to UI as a markdown format. + """Write a trial note to UI with streamlit as a markdown format. Args: study: The optuna study object. @@ -19,17 +19,30 @@ def render_trial_note(study: optuna.Study, trial: FrozenTrial) -> None: st.markdown(note["body"], unsafe_allow_html=True) -def render_widgets(study: optuna.Study, trial: FrozenTrial) -> None: - """Render user input widgets to UI. +def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> None: + """Render user input widgets to UI with streamlit. + + Submitted values to the forms are registered as each trial's user_attrs. Args: study: The optuna study object to get widget specification. trial: The optuna trial object to tell user feedbacks. + + Raises: + ValueError: If No form widgets registered. + ValueError: If 'output_type' of form widgets is not 'user_attr'. + ValueError: If any widget["type"] is not in ["choice", "slider", "text"]. """ form_widgets_dict = get_form_widgets_json(study.system_attrs) if form_widgets_dict is None: - return + raise ValueError("No form widgets registered.") + + if ( + "output_type" not in form_widgets_dict.keys() + or form_widgets_dict["output_type"] != "user_attr" + ): + raise ValueError("'output_type' should be 'user_attr'.") st.write("## Objective Form Widgets") @@ -60,23 +73,14 @@ def render_widgets(study: optuna.Study, trial: FrozenTrial) -> None: # TODO (kaitos): It is better to consider "optional". value = st.text_input(widget["description"]) values.append(value) - elif widget["type"] == "user_attr": - value = trial.user_attrs[widget["key"]] - values.append(value) else: - raise ValueError("Unsupported widget type.") + raise ValueError("Widget type should be 'choice', 'slider', or 'text'.") submitted = st.form_submit_button("Submit") - storage = study._storage - output_type = form_widgets_dict["output_type"] if submitted: - if output_type == "objective": - study.tell(trial.number, values) - elif output_type == "user_attr": - for widget, value in zip(widgets, values): - storage.set_trial_user_attr( - trial._trial_id, key=widget["user_attr_key"], value=value - ) - else: - st.warning("Detect unsupported output_type") + for widget, value in zip(widgets, values): + study._storage.set_trial_user_attr( + trial._trial_id, key=widget["user_attr_key"], value=value + ) # type: ignore + st.success("Feedback submitted!") From 17bcdf910e434629204b5bcbc56a3550818821f1 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 11:00:27 +0900 Subject: [PATCH 04/39] Fix some linter problems --- optuna_dashboard/_streamlit_helper.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index 2867673c..32e2f705 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -44,8 +44,6 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No ): raise ValueError("'output_type' should be 'user_attr'.") - st.write("## Objective Form Widgets") - widgets = form_widgets_dict["widgets"] values = [] with st.form("user_input", clear_on_submit=False): @@ -54,14 +52,14 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No value = st.radio( widget["description"], widget["values"], - format_func=lambda choice: widget["choices"][ + format_func=lambda choice, widget=widget: widget["choices"][ widget["values"].index(choice) - ], # noqa: B023 + ], horizontal=True, ) values.append(value) elif widget["type"] == "slider": - # NOTE: It is difficult to reflect "labels. + # NOTE: It is difficult to reflect "labels". value = st.slider( widget["description"], min_value=widget["min"], @@ -70,7 +68,8 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No ) values.append(value) elif widget["type"] == "text": - # TODO (kaitos): It is better to consider "optional". + # TODO (kaitos): Resolve that current implementation ignores "optional" + # (always optional on streamlit) value = st.text_input(widget["description"]) values.append(value) else: @@ -79,8 +78,11 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No if submitted: for widget, value in zip(widgets, values): + # "type: ignore" is required because "UserAttrRefJSON" has no key "user_attr_key" + # (Actually, widget type is limited to 'choice', 'slider', or 'text' in the above code. + # Therefore, this is not a problem to run this code, but mypy raises error.) study._storage.set_trial_user_attr( - trial._trial_id, key=widget["user_attr_key"], value=value - ) # type: ignore + trial._trial_id, key=widget["user_attr_key"], value=value # type: ignore + ) - st.success("Feedback submitted!") + st.success("Submitted!") From c7371418c0527db94924f5e05d6bcfe90aed8399 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 11:02:27 +0900 Subject: [PATCH 05/39] Add streamlit helper to __init__.py --- optuna_dashboard/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/optuna_dashboard/__init__.py b/optuna_dashboard/__init__.py index 0b2bc8a0..b47c36cf 100644 --- a/optuna_dashboard/__init__.py +++ b/optuna_dashboard/__init__.py @@ -13,6 +13,8 @@ from ._form_widget import TextInputWidget # noqa from ._named_objectives import set_objective_names # noqa from ._note import get_note # noqa from ._note import save_note # noqa +from ._streamlit_helper import render_trial_note # noqa +from ._streamlit_helper import render_user_attr_form_widgets # noqa __version__ = "0.10.3" From 4cdfcf1dce35d340f5c7ea5a8da1d4aba9081fc7 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 11:05:16 +0900 Subject: [PATCH 06/39] Update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 93259ae3..923eb45f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ optuna>=2.4 bottle scikit-learn +streamlit typing-extensions;python_version<"3.8" # lint From 603d1fcfded9ed373322530c970ed0fba37f63a3 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 11:08:50 +0900 Subject: [PATCH 07/39] update pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index be4a18e0..ffbf92a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "optuna>=2.4.0", "packaging", "scikit-learn", + "streamlit", ] dynamic = ["version"] From 86ff6e443152be6cf409937029b2f45c715dfef5 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 11:17:00 +0900 Subject: [PATCH 08/39] apply some fix for mypy --- optuna_dashboard/_streamlit_helper.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index 32e2f705..17027bd1 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -47,10 +47,12 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No widgets = form_widgets_dict["widgets"] values = [] with st.form("user_input", clear_on_submit=False): + for widget in widgets: + description = "" if widget["description"] is None else widget["description"] if widget["type"] == "choice": value = st.radio( - widget["description"], + description, widget["values"], format_func=lambda choice, widget=widget: widget["choices"][ widget["values"].index(choice) @@ -61,7 +63,7 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No elif widget["type"] == "slider": # NOTE: It is difficult to reflect "labels". value = st.slider( - widget["description"], + description, min_value=widget["min"], max_value=widget["max"], step=widget["step"], @@ -70,7 +72,7 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No elif widget["type"] == "text": # TODO (kaitos): Resolve that current implementation ignores "optional" # (always optional on streamlit) - value = st.text_input(widget["description"]) + value = st.text_input(description) values.append(value) else: raise ValueError("Widget type should be 'choice', 'slider', or 'text'.") @@ -81,8 +83,9 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No # "type: ignore" is required because "UserAttrRefJSON" has no key "user_attr_key" # (Actually, widget type is limited to 'choice', 'slider', or 'text' in the above code. # Therefore, this is not a problem to run this code, but mypy raises error.) - study._storage.set_trial_user_attr( - trial._trial_id, key=widget["user_attr_key"], value=value # type: ignore - ) + if widget["user_attr_key"] is not None: + study._storage.set_trial_user_attr( + trial._trial_id, key=widget["user_attr_key"], value=value + ) st.success("Submitted!") From 3d1c9dc42afa536db09ba68082c6e1a143e4e41d Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 11:18:59 +0900 Subject: [PATCH 09/39] Apply black --- optuna_dashboard/_streamlit_helper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index 17027bd1..7ae86fae 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -47,7 +47,6 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No widgets = form_widgets_dict["widgets"] values = [] with st.form("user_input", clear_on_submit=False): - for widget in widgets: description = "" if widget["description"] is None else widget["description"] if widget["type"] == "choice": From 1cbf27f15e52c685c1719ea99b9a8a6d2a0cec7a Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 11:25:45 +0900 Subject: [PATCH 10/39] fix some mypy error --- optuna_dashboard/_streamlit_helper.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index 7ae86fae..7876ac46 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -45,10 +45,13 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No raise ValueError("'output_type' should be 'user_attr'.") widgets = form_widgets_dict["widgets"] - values = [] + values: list[float | str] = [] with st.form("user_input", clear_on_submit=False): for widget in widgets: - description = "" if widget["description"] is None else widget["description"] + if widget["description"] is None: # type: ignore + description = "" + else: + description = widget["description"] # type: ignore if widget["type"] == "choice": value = st.radio( description, @@ -79,12 +82,9 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No if submitted: for widget, value in zip(widgets, values): - # "type: ignore" is required because "UserAttrRefJSON" has no key "user_attr_key" - # (Actually, widget type is limited to 'choice', 'slider', or 'text' in the above code. - # Therefore, this is not a problem to run this code, but mypy raises error.) - if widget["user_attr_key"] is not None: + if widget["user_attr_key"] is not None: # type: ignore study._storage.set_trial_user_attr( - trial._trial_id, key=widget["user_attr_key"], value=value + trial._trial_id, key=widget["user_attr_key"], value=value # type: ignore ) st.success("Submitted!") From 1f157776bb681598ace6c22cb85a639f96be977c Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 11:31:57 +0900 Subject: [PATCH 11/39] update type annotation --- optuna_dashboard/_streamlit_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index 7876ac46..9f70888d 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -45,7 +45,7 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No raise ValueError("'output_type' should be 'user_attr'.") widgets = form_widgets_dict["widgets"] - values: list[float | str] = [] + values: list[float | str | None] = [] with st.form("user_input", clear_on_submit=False): for widget in widgets: if widget["description"] is None: # type: ignore From 4e7d0bf17adfc06411079c15ad96bcd80ff68f17 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 11:35:43 +0900 Subject: [PATCH 12/39] remove type annotation --- optuna_dashboard/_streamlit_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index 9f70888d..df3c8321 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -45,7 +45,7 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No raise ValueError("'output_type' should be 'user_attr'.") widgets = form_widgets_dict["widgets"] - values: list[float | str | None] = [] + values = [] with st.form("user_input", clear_on_submit=False): for widget in widgets: if widget["description"] is None: # type: ignore From fb87b235ff48f1cec30f6a647d8de0ff26470e70 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 11:39:20 +0900 Subject: [PATCH 13/39] add type:ignore --- optuna_dashboard/_streamlit_helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index df3c8321..bcc6c24b 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -56,7 +56,7 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No value = st.radio( description, widget["values"], - format_func=lambda choice, widget=widget: widget["choices"][ + format_func=lambda choice, widget=widget: widget["choices"][ # type: ignore widget["values"].index(choice) ], horizontal=True, @@ -75,7 +75,7 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No # TODO (kaitos): Resolve that current implementation ignores "optional" # (always optional on streamlit) value = st.text_input(description) - values.append(value) + values.append(value) # type: ignore else: raise ValueError("Widget type should be 'choice', 'slider', or 'text'.") submitted = st.form_submit_button("Submit") From 2430ff6bf3f0eaba37a3794f091559714d773b9b Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 11:44:30 +0900 Subject: [PATCH 14/39] update --- optuna_dashboard/_streamlit_helper.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index bcc6c24b..1d25f7a7 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -74,8 +74,7 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No elif widget["type"] == "text": # TODO (kaitos): Resolve that current implementation ignores "optional" # (always optional on streamlit) - value = st.text_input(description) - values.append(value) # type: ignore + values.append(st.text_input(description)) else: raise ValueError("Widget type should be 'choice', 'slider', or 'text'.") submitted = st.form_submit_button("Submit") From 9eeffba342844deb34a44b0a73ce1172b2d55a9c Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 11:46:48 +0900 Subject: [PATCH 15/39] add type: ignore --- optuna_dashboard/_streamlit_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index 1d25f7a7..51575958 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -74,7 +74,7 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No elif widget["type"] == "text": # TODO (kaitos): Resolve that current implementation ignores "optional" # (always optional on streamlit) - values.append(st.text_input(description)) + values.append(st.text_input(description)) # type: ignore else: raise ValueError("Widget type should be 'choice', 'slider', or 'text'.") submitted = st.form_submit_button("Submit") From 5645ca5393cbc1077518c8cfd5b939b1e3e8534d Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 13:59:01 +0900 Subject: [PATCH 16/39] fix some mypy error --- optuna_dashboard/_streamlit_helper.py | 126 ++++++++++++++++++-------- 1 file changed, 89 insertions(+), 37 deletions(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index 51575958..daf56271 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -4,7 +4,10 @@ import optuna from optuna.trial import FrozenTrial import streamlit as st +from ._form_widget import ChoiceWidgetJSON from ._form_widget import get_form_widgets_json +from ._form_widget import SliderWidgetJSON +from ._form_widget import TextInputWidgetJSON from ._note import get_note_from_system_attrs @@ -19,44 +22,22 @@ def render_trial_note(study: optuna.Study, trial: FrozenTrial) -> None: st.markdown(note["body"], unsafe_allow_html=True) -def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> None: - """Render user input widgets to UI with streamlit. - - Submitted values to the forms are registered as each trial's user_attrs. - - Args: - study: The optuna study object to get widget specification. - trial: The optuna trial object to tell user feedbacks. - - Raises: - ValueError: If No form widgets registered. - ValueError: If 'output_type' of form widgets is not 'user_attr'. - ValueError: If any widget["type"] is not in ["choice", "slider", "text"]. - """ - - form_widgets_dict = get_form_widgets_json(study.system_attrs) - if form_widgets_dict is None: - raise ValueError("No form widgets registered.") - - if ( - "output_type" not in form_widgets_dict.keys() - or form_widgets_dict["output_type"] != "user_attr" - ): - raise ValueError("'output_type' should be 'user_attr'.") - - widgets = form_widgets_dict["widgets"] +def _render_widgets( + widgets: list[ChoiceWidgetJSON | SliderWidgetJSON | TextInputWidgetJSON], +) -> tuple[bool, list[str]]: values = [] with st.form("user_input", clear_on_submit=False): for widget in widgets: - if widget["description"] is None: # type: ignore + if widget["description"] is None: description = "" else: - description = widget["description"] # type: ignore + description = widget["description"] + if widget["type"] == "choice": value = st.radio( description, widget["values"], - format_func=lambda choice, widget=widget: widget["choices"][ # type: ignore + format_func=lambda choice, widget=widget: widget["choices"][ widget["values"].index(choice) ], horizontal=True, @@ -72,18 +53,89 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No ) values.append(value) elif widget["type"] == "text": - # TODO (kaitos): Resolve that current implementation ignores "optional" - # (always optional on streamlit) - values.append(st.text_input(description)) # type: ignore + # NOTE: Current implementation ignores "optional". + value = st.text_input(description) + values.append(value) else: - raise ValueError("Widget type should be 'choice', 'slider', or 'text'.") + raise ValueError("Widget type should be 'choice', 'slider' or 'text'.") submitted = st.form_submit_button("Submit") + return submitted, values + + +def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> None: + """Render user input widgets to UI with streamlit. + + Submitted values to the forms are registered as each trial's user_attrs. + "type" of widgets should be "choice", "slider", or "text". + + Args: + study: The optuna study object to get widget specification. + trial: The optuna trial object to save user feedbacks. + + Raises: + ValueError: If No form widgets registered. + ValueError: If 'output_type' of form widgets is not 'user_attr'. + ValueError: If any widget['type'] is not in ['choice', 'slider', 'text']. + ValueError: if any widget does not have 'user_attr_key'. + + """ + + form_widgets_dict = get_form_widgets_json(study.system_attrs) + if form_widgets_dict is None: + raise ValueError("No form widgets registered.") + + if form_widgets_dict["output_type"] != "user_attr": + raise ValueError("'output_type' should be 'user_attr'.") + + widgets = form_widgets_dict["widgets"] + for widget in widgets: + if widget["type"] not in ["choice", "slider", "text"]: + raise ValueError("Widget type should be 'choice', 'slider' or 'text'.") + if widget["user_attr_key"] is None: # type: ignore + raise ValueError("Widget should have 'user_attr_key'.") + + submitted, values = _render_widgets(widget) # type: ignore if submitted: for widget, value in zip(widgets, values): - if widget["user_attr_key"] is not None: # type: ignore - study._storage.set_trial_user_attr( - trial._trial_id, key=widget["user_attr_key"], value=value # type: ignore - ) + study._storage.set_trial_user_attr( + trial._trial_id, key=widget["user_attr_key"], value=value # type: ignore + ) st.success("Submitted!") + + +def render_objective_form_widgets(study: optuna.Study, trial: FrozenTrial) -> None: + """Render user input widgets to UI with streamlit. + + Submitted values to the forms are telled to optuna trial object. + "type" of widgets should be "choice" or "slider". + Multiple widgets correspond to multi-objective optimization. + + Args: + study: The optuna study object to get widget specification. + trial: The optuna trial object to tell user feedbacks. + + Raises: + ValueError: If No form widgets registered. + ValueError: If 'output_type' of form widgets is not 'objective'. + ValueError: If any widget['type'] is not in ['choice', 'slider']. + """ + + form_widgets_dict = get_form_widgets_json(study.system_attrs) + if form_widgets_dict is None: + raise ValueError("No form widgets registered.") + + if form_widgets_dict["output_type"] != "user_attr": + raise ValueError("'output_type' should be 'user_attr'.") + + widgets = form_widgets_dict["widgets"] + for widget in widgets: + if widget["type"] not in ["choice", "slider"]: + raise ValueError("Widget type should be 'choice' or 'slider'.") + + submitted, values = _render_widgets(widgets) # type: ignore + + if submitted: + study.tell(trial.number, values) + st.success("Submitted!") From 68ab4eb7b3285098433333172271562ebca93d0a Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 14:02:42 +0900 Subject: [PATCH 17/39] roll back __init__.py --- optuna_dashboard/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/optuna_dashboard/__init__.py b/optuna_dashboard/__init__.py index b47c36cf..4e32912c 100644 --- a/optuna_dashboard/__init__.py +++ b/optuna_dashboard/__init__.py @@ -13,8 +13,5 @@ from ._form_widget import TextInputWidget # noqa from ._named_objectives import set_objective_names # noqa from ._note import get_note # noqa from ._note import save_note # noqa -from ._streamlit_helper import render_trial_note # noqa -from ._streamlit_helper import render_user_attr_form_widgets # noqa - __version__ = "0.10.3" From 09231c59ddfd7c92d17a74a91ff6f1d737514fdc Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 14:03:03 +0900 Subject: [PATCH 18/39] misc --- optuna_dashboard/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/optuna_dashboard/__init__.py b/optuna_dashboard/__init__.py index 4e32912c..0b2bc8a0 100644 --- a/optuna_dashboard/__init__.py +++ b/optuna_dashboard/__init__.py @@ -14,4 +14,5 @@ from ._named_objectives import set_objective_names # noqa from ._note import get_note # noqa from ._note import save_note # noqa + __version__ = "0.10.3" From ceb7546981bf784cf061a73835045f051a90f20e Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 14:17:00 +0900 Subject: [PATCH 19/39] fix 1 mypy error --- optuna_dashboard/_streamlit_helper.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index daf56271..05903e6e 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -22,6 +22,10 @@ def render_trial_note(study: optuna.Study, trial: FrozenTrial) -> None: st.markdown(note["body"], unsafe_allow_html=True) +def _format_choice(choice: float, widget: ChoiceWidgetJSON) -> str: + return widget["choices"][widget["values"].index(choice)] + + def _render_widgets( widgets: list[ChoiceWidgetJSON | SliderWidgetJSON | TextInputWidgetJSON], ) -> tuple[bool, list[str]]: @@ -37,9 +41,7 @@ def _render_widgets( value = st.radio( description, widget["values"], - format_func=lambda choice, widget=widget: widget["choices"][ - widget["values"].index(choice) - ], + format_func=lambda choice, widget=widget: _format_choice(choice, widget), horizontal=True, ) values.append(value) From a0ed9cc86efe13fdd5540e3c3272fd99f7ab3eff Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 14:31:42 +0900 Subject: [PATCH 20/39] Add type: ignore --- optuna_dashboard/_streamlit_helper.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index 05903e6e..5155fd07 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -41,7 +41,9 @@ def _render_widgets( value = st.radio( description, widget["values"], - format_func=lambda choice, widget=widget: _format_choice(choice, widget), + format_func=lambda choice, widget=widget: _format_choice( + choice, widget + ), # type: ignore horizontal=True, ) values.append(value) @@ -139,5 +141,5 @@ def render_objective_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No submitted, values = _render_widgets(widgets) # type: ignore if submitted: - study.tell(trial.number, values) + study.tell(trial.number, values) # type: ignore st.success("Submitted!") From 01433d2a637bbe2912f815d6cb5b93fa4e286577 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 14:38:38 +0900 Subject: [PATCH 21/39] move type: ignore --- optuna_dashboard/_streamlit_helper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index 5155fd07..b3642409 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -28,7 +28,7 @@ def _format_choice(choice: float, widget: ChoiceWidgetJSON) -> str: def _render_widgets( widgets: list[ChoiceWidgetJSON | SliderWidgetJSON | TextInputWidgetJSON], -) -> tuple[bool, list[str]]: +) -> tuple[bool, list[str | float]]: values = [] with st.form("user_input", clear_on_submit=False): for widget in widgets: @@ -41,9 +41,9 @@ def _render_widgets( value = st.radio( description, widget["values"], - format_func=lambda choice, widget=widget: _format_choice( + format_func=lambda choice, widget=widget: _format_choice( # type: ignore choice, widget - ), # type: ignore + ), horizontal=True, ) values.append(value) From 56c6d72c4555a519557541f655ddaf5660082b30 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 14:52:04 +0900 Subject: [PATCH 22/39] Add type annotation --- optuna_dashboard/_streamlit_helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index b3642409..eeb28542 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -28,8 +28,8 @@ def _format_choice(choice: float, widget: ChoiceWidgetJSON) -> str: def _render_widgets( widgets: list[ChoiceWidgetJSON | SliderWidgetJSON | TextInputWidgetJSON], -) -> tuple[bool, list[str | float]]: - values = [] +) -> tuple[bool, list[str | float | None]]: + values: list[str | float | None] = [] with st.form("user_input", clear_on_submit=False): for widget in widgets: if widget["description"] is None: From 2f8403a60126a4ab0c0785e6d605494803e9febf Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 15:03:54 +0900 Subject: [PATCH 23/39] trial for mypy error --- optuna_dashboard/_streamlit_helper.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/optuna_dashboard/_streamlit_helper.py b/optuna_dashboard/_streamlit_helper.py index eeb28542..9a924698 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/optuna_dashboard/_streamlit_helper.py @@ -58,8 +58,7 @@ def _render_widgets( values.append(value) elif widget["type"] == "text": # NOTE: Current implementation ignores "optional". - value = st.text_input(description) - values.append(value) + values.append(st.text_input(description)) else: raise ValueError("Widget type should be 'choice', 'slider' or 'text'.") submitted = st.form_submit_button("Submit") From 6395929885c8240c062c667a14a435e0abad3d25 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 15:14:13 +0900 Subject: [PATCH 24/39] Change dir structure --- pyproject.toml | 1 - requirements.txt | 1 - streamlit/__init__.py | 3 +++ {optuna_dashboard => streamlit}/_streamlit_helper.py | 11 +++++------ 4 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 streamlit/__init__.py rename {optuna_dashboard => streamlit}/_streamlit_helper.py (94%) diff --git a/pyproject.toml b/pyproject.toml index ffbf92a5..be4a18e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,6 @@ dependencies = [ "optuna>=2.4.0", "packaging", "scikit-learn", - "streamlit", ] dynamic = ["version"] diff --git a/requirements.txt b/requirements.txt index 923eb45f..93259ae3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ optuna>=2.4 bottle scikit-learn -streamlit typing-extensions;python_version<"3.8" # lint diff --git a/streamlit/__init__.py b/streamlit/__init__.py new file mode 100644 index 00000000..c3e8431e --- /dev/null +++ b/streamlit/__init__.py @@ -0,0 +1,3 @@ +from ._streamlit_helper import render_objective_form_widgets +from ._streamlit_helper import render_trial_note +from ._streamlit_helper import render_user_attr_form_widgets diff --git a/optuna_dashboard/_streamlit_helper.py b/streamlit/_streamlit_helper.py similarity index 94% rename from optuna_dashboard/_streamlit_helper.py rename to streamlit/_streamlit_helper.py index 9a924698..36a01f43 100644 --- a/optuna_dashboard/_streamlit_helper.py +++ b/streamlit/_streamlit_helper.py @@ -2,14 +2,13 @@ from __future__ import annotations import optuna from optuna.trial import FrozenTrial +from optuna_dashboard._form_widget import ChoiceWidgetJSON +from optuna_dashboard._form_widget import get_form_widgets_json +from optuna_dashboard._form_widget import SliderWidgetJSON +from optuna_dashboard._form_widget import TextInputWidgetJSON +from optuna_dashboard._note import get_note_from_system_attrs import streamlit as st -from ._form_widget import ChoiceWidgetJSON -from ._form_widget import get_form_widgets_json -from ._form_widget import SliderWidgetJSON -from ._form_widget import TextInputWidgetJSON -from ._note import get_note_from_system_attrs - def render_trial_note(study: optuna.Study, trial: FrozenTrial) -> None: """Write a trial note to UI with streamlit as a markdown format. From da3423c778e1d276c503e5d1ab8ae6627d1353bd Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 15:20:32 +0900 Subject: [PATCH 25/39] change file name --- streamlit_helper/__init__.py | 3 + streamlit_helper/_streamlit_helper.py | 143 ++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 streamlit_helper/__init__.py create mode 100644 streamlit_helper/_streamlit_helper.py diff --git a/streamlit_helper/__init__.py b/streamlit_helper/__init__.py new file mode 100644 index 00000000..c3e8431e --- /dev/null +++ b/streamlit_helper/__init__.py @@ -0,0 +1,3 @@ +from ._streamlit_helper import render_objective_form_widgets +from ._streamlit_helper import render_trial_note +from ._streamlit_helper import render_user_attr_form_widgets diff --git a/streamlit_helper/_streamlit_helper.py b/streamlit_helper/_streamlit_helper.py new file mode 100644 index 00000000..e627b8a9 --- /dev/null +++ b/streamlit_helper/_streamlit_helper.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import optuna +from optuna.trial import FrozenTrial +from optuna_dashboard import ChoiceWidgetJSON +from optuna_dashboard._form_widget import get_form_widgets_json +from optuna_dashboard._form_widget import SliderWidgetJSON +from optuna_dashboard._form_widget import TextInputWidgetJSON +from optuna_dashboard._note import get_note_from_system_attrs +import streamlit as st + + +def render_trial_note(study: optuna.Study, trial: FrozenTrial) -> None: + """Write a trial note to UI with streamlit as a markdown format. + + Args: + study: The optuna study object. + trial: The optuna trial object to get note. + """ + note = get_note_from_system_attrs(study.system_attrs, trial._trial_id) + st.markdown(note["body"], unsafe_allow_html=True) + + +def _format_choice(choice: float, widget: ChoiceWidgetJSON) -> str: + return widget["choices"][widget["values"].index(choice)] + + +def _render_widgets( + widgets: list[ChoiceWidgetJSON | SliderWidgetJSON | TextInputWidgetJSON], +) -> tuple[bool, list[str | float | None]]: + values: list[str | float | None] = [] + with st.form("user_input", clear_on_submit=False): + for widget in widgets: + if widget["description"] is None: + description = "" + else: + description = widget["description"] + + if widget["type"] == "choice": + value = st.radio( + description, + widget["values"], + format_func=lambda choice, widget=widget: _format_choice( # type: ignore + choice, widget + ), + horizontal=True, + ) + values.append(value) + elif widget["type"] == "slider": + # NOTE: It is difficult to reflect "labels". + value = st.slider( + description, + min_value=widget["min"], + max_value=widget["max"], + step=widget["step"], + ) + values.append(value) + elif widget["type"] == "text": + # NOTE: Current implementation ignores "optional". + values.append(st.text_input(description)) + else: + raise ValueError("Widget type should be 'choice', 'slider' or 'text'.") + submitted = st.form_submit_button("Submit") + return submitted, values + + +def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> None: + """Render user input widgets to UI with streamlit. + + Submitted values to the forms are registered as each trial's user_attrs. + "type" of widgets should be "choice", "slider", or "text". + + Args: + study: The optuna study object to get widget specification. + trial: The optuna trial object to save user feedbacks. + + Raises: + ValueError: If No form widgets registered. + ValueError: If 'output_type' of form widgets is not 'user_attr'. + ValueError: If any widget['type'] is not in ['choice', 'slider', 'text']. + ValueError: if any widget does not have 'user_attr_key'. + + """ + + form_widgets_dict = get_form_widgets_json(study.system_attrs) + if form_widgets_dict is None: + raise ValueError("No form widgets registered.") + + if form_widgets_dict["output_type"] != "user_attr": + raise ValueError("'output_type' should be 'user_attr'.") + + widgets = form_widgets_dict["widgets"] + for widget in widgets: + if widget["type"] not in ["choice", "slider", "text"]: + raise ValueError("Widget type should be 'choice', 'slider' or 'text'.") + if widget["user_attr_key"] is None: # type: ignore + raise ValueError("Widget should have 'user_attr_key'.") + + submitted, values = _render_widgets(widget) # type: ignore + + if submitted: + for widget, value in zip(widgets, values): + study._storage.set_trial_user_attr( + trial._trial_id, key=widget["user_attr_key"], value=value # type: ignore + ) + + st.success("Submitted!") + + +def render_objective_form_widgets(study: optuna.Study, trial: FrozenTrial) -> None: + """Render user input widgets to UI with streamlit. + + Submitted values to the forms are telled to optuna trial object. + "type" of widgets should be "choice" or "slider". + Multiple widgets correspond to multi-objective optimization. + + Args: + study: The optuna study object to get widget specification. + trial: The optuna trial object to tell user feedbacks. + + Raises: + ValueError: If No form widgets registered. + ValueError: If 'output_type' of form widgets is not 'objective'. + ValueError: If any widget['type'] is not in ['choice', 'slider']. + """ + + form_widgets_dict = get_form_widgets_json(study.system_attrs) + if form_widgets_dict is None: + raise ValueError("No form widgets registered.") + + if form_widgets_dict["output_type"] != "user_attr": + raise ValueError("'output_type' should be 'user_attr'.") + + widgets = form_widgets_dict["widgets"] + for widget in widgets: + if widget["type"] not in ["choice", "slider"]: + raise ValueError("Widget type should be 'choice' or 'slider'.") + + submitted, values = _render_widgets(widgets) # type: ignore + + if submitted: + study.tell(trial.number, values) # type: ignore + st.success("Submitted!") From 85bf158144229147a22262e9c302379d878eab10 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 30 Jun 2023 15:47:39 +0900 Subject: [PATCH 26/39] Fix import path --- optuna_dashboard/streamlit/__init__.py | 3 + .../streamlit}/_streamlit_helper.py | 15 +- streamlit/__init__.py | 3 - streamlit/_streamlit_helper.py | 143 ------------------ streamlit_helper/__init__.py | 3 - 5 files changed, 13 insertions(+), 154 deletions(-) create mode 100644 optuna_dashboard/streamlit/__init__.py rename {streamlit_helper => optuna_dashboard/streamlit}/_streamlit_helper.py (94%) delete mode 100644 streamlit/__init__.py delete mode 100644 streamlit/_streamlit_helper.py delete mode 100644 streamlit_helper/__init__.py diff --git a/optuna_dashboard/streamlit/__init__.py b/optuna_dashboard/streamlit/__init__.py new file mode 100644 index 00000000..387a9c16 --- /dev/null +++ b/optuna_dashboard/streamlit/__init__.py @@ -0,0 +1,3 @@ +from ._streamlit_helper import render_objective_form_widgets # noqa +from ._streamlit_helper import render_trial_note # noqa +from ._streamlit_helper import render_user_attr_form_widgets # noqa diff --git a/streamlit_helper/_streamlit_helper.py b/optuna_dashboard/streamlit/_streamlit_helper.py similarity index 94% rename from streamlit_helper/_streamlit_helper.py rename to optuna_dashboard/streamlit/_streamlit_helper.py index e627b8a9..86c64602 100644 --- a/streamlit_helper/_streamlit_helper.py +++ b/optuna_dashboard/streamlit/_streamlit_helper.py @@ -1,15 +1,20 @@ from __future__ import annotations import optuna +from typing import TYPE_CHECKING + from optuna.trial import FrozenTrial -from optuna_dashboard import ChoiceWidgetJSON -from optuna_dashboard._form_widget import get_form_widgets_json -from optuna_dashboard._form_widget import SliderWidgetJSON -from optuna_dashboard._form_widget import TextInputWidgetJSON -from optuna_dashboard._note import get_note_from_system_attrs +from .._form_widget import get_form_widgets_json +from .._note import get_note_from_system_attrs import streamlit as st +if TYPE_CHECKING: + from .._form_widget import TextInputWidgetJSON + from .._form_widget import ChoiceWidgetJSON + from .._form_widget import SliderWidgetJSON + + def render_trial_note(study: optuna.Study, trial: FrozenTrial) -> None: """Write a trial note to UI with streamlit as a markdown format. diff --git a/streamlit/__init__.py b/streamlit/__init__.py deleted file mode 100644 index c3e8431e..00000000 --- a/streamlit/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from ._streamlit_helper import render_objective_form_widgets -from ._streamlit_helper import render_trial_note -from ._streamlit_helper import render_user_attr_form_widgets diff --git a/streamlit/_streamlit_helper.py b/streamlit/_streamlit_helper.py deleted file mode 100644 index 36a01f43..00000000 --- a/streamlit/_streamlit_helper.py +++ /dev/null @@ -1,143 +0,0 @@ -from __future__ import annotations - -import optuna -from optuna.trial import FrozenTrial -from optuna_dashboard._form_widget import ChoiceWidgetJSON -from optuna_dashboard._form_widget import get_form_widgets_json -from optuna_dashboard._form_widget import SliderWidgetJSON -from optuna_dashboard._form_widget import TextInputWidgetJSON -from optuna_dashboard._note import get_note_from_system_attrs -import streamlit as st - - -def render_trial_note(study: optuna.Study, trial: FrozenTrial) -> None: - """Write a trial note to UI with streamlit as a markdown format. - - Args: - study: The optuna study object. - trial: The optuna trial object to get note. - """ - note = get_note_from_system_attrs(study.system_attrs, trial._trial_id) - st.markdown(note["body"], unsafe_allow_html=True) - - -def _format_choice(choice: float, widget: ChoiceWidgetJSON) -> str: - return widget["choices"][widget["values"].index(choice)] - - -def _render_widgets( - widgets: list[ChoiceWidgetJSON | SliderWidgetJSON | TextInputWidgetJSON], -) -> tuple[bool, list[str | float | None]]: - values: list[str | float | None] = [] - with st.form("user_input", clear_on_submit=False): - for widget in widgets: - if widget["description"] is None: - description = "" - else: - description = widget["description"] - - if widget["type"] == "choice": - value = st.radio( - description, - widget["values"], - format_func=lambda choice, widget=widget: _format_choice( # type: ignore - choice, widget - ), - horizontal=True, - ) - values.append(value) - elif widget["type"] == "slider": - # NOTE: It is difficult to reflect "labels". - value = st.slider( - description, - min_value=widget["min"], - max_value=widget["max"], - step=widget["step"], - ) - values.append(value) - elif widget["type"] == "text": - # NOTE: Current implementation ignores "optional". - values.append(st.text_input(description)) - else: - raise ValueError("Widget type should be 'choice', 'slider' or 'text'.") - submitted = st.form_submit_button("Submit") - return submitted, values - - -def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> None: - """Render user input widgets to UI with streamlit. - - Submitted values to the forms are registered as each trial's user_attrs. - "type" of widgets should be "choice", "slider", or "text". - - Args: - study: The optuna study object to get widget specification. - trial: The optuna trial object to save user feedbacks. - - Raises: - ValueError: If No form widgets registered. - ValueError: If 'output_type' of form widgets is not 'user_attr'. - ValueError: If any widget['type'] is not in ['choice', 'slider', 'text']. - ValueError: if any widget does not have 'user_attr_key'. - - """ - - form_widgets_dict = get_form_widgets_json(study.system_attrs) - if form_widgets_dict is None: - raise ValueError("No form widgets registered.") - - if form_widgets_dict["output_type"] != "user_attr": - raise ValueError("'output_type' should be 'user_attr'.") - - widgets = form_widgets_dict["widgets"] - for widget in widgets: - if widget["type"] not in ["choice", "slider", "text"]: - raise ValueError("Widget type should be 'choice', 'slider' or 'text'.") - if widget["user_attr_key"] is None: # type: ignore - raise ValueError("Widget should have 'user_attr_key'.") - - submitted, values = _render_widgets(widget) # type: ignore - - if submitted: - for widget, value in zip(widgets, values): - study._storage.set_trial_user_attr( - trial._trial_id, key=widget["user_attr_key"], value=value # type: ignore - ) - - st.success("Submitted!") - - -def render_objective_form_widgets(study: optuna.Study, trial: FrozenTrial) -> None: - """Render user input widgets to UI with streamlit. - - Submitted values to the forms are telled to optuna trial object. - "type" of widgets should be "choice" or "slider". - Multiple widgets correspond to multi-objective optimization. - - Args: - study: The optuna study object to get widget specification. - trial: The optuna trial object to tell user feedbacks. - - Raises: - ValueError: If No form widgets registered. - ValueError: If 'output_type' of form widgets is not 'objective'. - ValueError: If any widget['type'] is not in ['choice', 'slider']. - """ - - form_widgets_dict = get_form_widgets_json(study.system_attrs) - if form_widgets_dict is None: - raise ValueError("No form widgets registered.") - - if form_widgets_dict["output_type"] != "user_attr": - raise ValueError("'output_type' should be 'user_attr'.") - - widgets = form_widgets_dict["widgets"] - for widget in widgets: - if widget["type"] not in ["choice", "slider"]: - raise ValueError("Widget type should be 'choice' or 'slider'.") - - submitted, values = _render_widgets(widgets) # type: ignore - - if submitted: - study.tell(trial.number, values) # type: ignore - st.success("Submitted!") diff --git a/streamlit_helper/__init__.py b/streamlit_helper/__init__.py deleted file mode 100644 index c3e8431e..00000000 --- a/streamlit_helper/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from ._streamlit_helper import render_objective_form_widgets -from ._streamlit_helper import render_trial_note -from ._streamlit_helper import render_user_attr_form_widgets From 87be6de36583827dc1616454fb6027c3f381b57c Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 7 Jul 2023 09:52:24 +0900 Subject: [PATCH 27/39] update some code structure --- .../streamlit/_streamlit_helper.py | 79 ++++++++++--------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/optuna_dashboard/streamlit/_streamlit_helper.py b/optuna_dashboard/streamlit/_streamlit_helper.py index 86c64602..0e128920 100644 --- a/optuna_dashboard/streamlit/_streamlit_helper.py +++ b/optuna_dashboard/streamlit/_streamlit_helper.py @@ -1,18 +1,22 @@ from __future__ import annotations -import optuna from typing import TYPE_CHECKING +import optuna from optuna.trial import FrozenTrial +import streamlit as st + from .._form_widget import get_form_widgets_json from .._note import get_note_from_system_attrs -import streamlit as st if TYPE_CHECKING: - from .._form_widget import TextInputWidgetJSON + from typing import Sequence + from .._form_widget import ChoiceWidgetJSON from .._form_widget import SliderWidgetJSON + from .._form_widget import TextInputWidgetJSON + from .._form_widget import UserAttrRefJSON def render_trial_note(study: optuna.Study, trial: FrozenTrial) -> None: @@ -30,41 +34,45 @@ def _format_choice(choice: float, widget: ChoiceWidgetJSON) -> str: return widget["choices"][widget["values"].index(choice)] +def _format_description(description: str | None) -> str: + return "" if description is None else description + + def _render_widgets( - widgets: list[ChoiceWidgetJSON | SliderWidgetJSON | TextInputWidgetJSON], + widgets: Sequence[ChoiceWidgetJSON | SliderWidgetJSON | TextInputWidgetJSON | UserAttrRefJSON], + trial: FrozenTrial, ) -> tuple[bool, list[str | float | None]]: values: list[str | float | None] = [] + with st.form("user_input", clear_on_submit=False): for widget in widgets: - if widget["description"] is None: - description = "" - else: - description = widget["description"] - if widget["type"] == "choice": value = st.radio( - description, + _format_description(widget["description"]), widget["values"], format_func=lambda choice, widget=widget: _format_choice( # type: ignore choice, widget ), horizontal=True, ) - values.append(value) elif widget["type"] == "slider": # NOTE: It is difficult to reflect "labels". value = st.slider( - description, + _format_description(widget["description"]), min_value=widget["min"], max_value=widget["max"], step=widget["step"], ) - values.append(value) elif widget["type"] == "text": # NOTE: Current implementation ignores "optional". - values.append(st.text_input(description)) + value = st.text_input(_format_description(widget["description"])) + elif widget["type"] == "user_attr": + value = trial.user_attrs[widget["key"]] else: - raise ValueError("Widget type should be 'choice', 'slider' or 'text'.") + raise ValueError( + "Widget type should be 'choice', 'slider', 'text', or 'user_attr'." + ) + values.append(value) submitted = st.form_submit_button("Submit") return submitted, values @@ -73,7 +81,6 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No """Render user input widgets to UI with streamlit. Submitted values to the forms are registered as each trial's user_attrs. - "type" of widgets should be "choice", "slider", or "text". Args: study: The optuna study object to get widget specification. @@ -82,8 +89,6 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No Raises: ValueError: If No form widgets registered. ValueError: If 'output_type' of form widgets is not 'user_attr'. - ValueError: If any widget['type'] is not in ['choice', 'slider', 'text']. - ValueError: if any widget does not have 'user_attr_key'. """ @@ -95,19 +100,14 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No raise ValueError("'output_type' should be 'user_attr'.") widgets = form_widgets_dict["widgets"] - for widget in widgets: - if widget["type"] not in ["choice", "slider", "text"]: - raise ValueError("Widget type should be 'choice', 'slider' or 'text'.") - if widget["user_attr_key"] is None: # type: ignore - raise ValueError("Widget should have 'user_attr_key'.") - - submitted, values = _render_widgets(widget) # type: ignore + submitted, values = _render_widgets(widgets, trial) if submitted: for widget, value in zip(widgets, values): - study._storage.set_trial_user_attr( - trial._trial_id, key=widget["user_attr_key"], value=value # type: ignore - ) + if "user_attr_key" in widget.keys(): + study._storage.set_trial_user_attr( + trial._trial_id, key=widget["user_attr_key"], value=value # type: ignore + ) st.success("Submitted!") @@ -116,7 +116,7 @@ def render_objective_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No """Render user input widgets to UI with streamlit. Submitted values to the forms are telled to optuna trial object. - "type" of widgets should be "choice" or "slider". + All submitted values should be float. Multiple widgets correspond to multi-objective optimization. Args: @@ -126,23 +126,24 @@ def render_objective_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No Raises: ValueError: If No form widgets registered. ValueError: If 'output_type' of form widgets is not 'objective'. - ValueError: If any widget['type'] is not in ['choice', 'slider']. + ValueError: If any submitted values cannot be converted to float. """ form_widgets_dict = get_form_widgets_json(study.system_attrs) if form_widgets_dict is None: raise ValueError("No form widgets registered.") - if form_widgets_dict["output_type"] != "user_attr": - raise ValueError("'output_type' should be 'user_attr'.") + if form_widgets_dict["output_type"] != "objective": + raise ValueError("'output_type' should be 'objective'.") - widgets = form_widgets_dict["widgets"] - for widget in widgets: - if widget["type"] not in ["choice", "slider"]: - raise ValueError("Widget type should be 'choice' or 'slider'.") - - submitted, values = _render_widgets(widgets) # type: ignore + submitted, values = _render_widgets(form_widgets_dict["widgets"], trial) if submitted: - study.tell(trial.number, values) # type: ignore + values_float = [] + for value in values: + try: + values_float.append(float(value)) # type: ignore + except ValueError as e: + raise ValueError("All submitted values should be float.") from e + study.tell(trial.number, values_float) st.success("Submitted!") From fffe3df650319a965c7ef9087aa42c277a8f7936 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 7 Jul 2023 10:47:25 +0900 Subject: [PATCH 28/39] Remove unused space --- optuna_dashboard/streamlit/_streamlit_helper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/optuna_dashboard/streamlit/_streamlit_helper.py b/optuna_dashboard/streamlit/_streamlit_helper.py index 0e128920..6d23e3dc 100644 --- a/optuna_dashboard/streamlit/_streamlit_helper.py +++ b/optuna_dashboard/streamlit/_streamlit_helper.py @@ -89,7 +89,6 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No Raises: ValueError: If No form widgets registered. ValueError: If 'output_type' of form widgets is not 'user_attr'. - """ form_widgets_dict = get_form_widgets_json(study.system_attrs) From 96ee424aeaeacbfdded6967ea6598bc383c5724c Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 7 Jul 2023 10:50:04 +0900 Subject: [PATCH 29/39] Add some space for readability --- optuna_dashboard/streamlit/_streamlit_helper.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/streamlit/_streamlit_helper.py b/optuna_dashboard/streamlit/_streamlit_helper.py index 6d23e3dc..719b5c0a 100644 --- a/optuna_dashboard/streamlit/_streamlit_helper.py +++ b/optuna_dashboard/streamlit/_streamlit_helper.py @@ -26,6 +26,7 @@ def render_trial_note(study: optuna.Study, trial: FrozenTrial) -> None: study: The optuna study object. trial: The optuna trial object to get note. """ + note = get_note_from_system_attrs(study.system_attrs, trial._trial_id) st.markdown(note["body"], unsafe_allow_html=True) @@ -73,6 +74,7 @@ def _render_widgets( "Widget type should be 'choice', 'slider', 'text', or 'user_attr'." ) values.append(value) + submitted = st.form_submit_button("Submit") return submitted, values @@ -94,7 +96,6 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No form_widgets_dict = get_form_widgets_json(study.system_attrs) if form_widgets_dict is None: raise ValueError("No form widgets registered.") - if form_widgets_dict["output_type"] != "user_attr": raise ValueError("'output_type' should be 'user_attr'.") @@ -131,7 +132,6 @@ def render_objective_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No form_widgets_dict = get_form_widgets_json(study.system_attrs) if form_widgets_dict is None: raise ValueError("No form widgets registered.") - if form_widgets_dict["output_type"] != "objective": raise ValueError("'output_type' should be 'objective'.") @@ -144,5 +144,6 @@ def render_objective_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No values_float.append(float(value)) # type: ignore except ValueError as e: raise ValueError("All submitted values should be float.") from e + study.tell(trial.number, values_float) st.success("Submitted!") From 836382c915b03477d35d8cd25e6b8bdd2ac2c811 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 7 Jul 2023 12:10:54 +0900 Subject: [PATCH 30/39] Add unit tests --- .../streamlit/_streamlit_helper.py | 2 +- python_tests/streamlit/__init__.py | 0 .../streamlit/test_streamlit_helper.py | 98 +++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 python_tests/streamlit/__init__.py create mode 100644 python_tests/streamlit/test_streamlit_helper.py diff --git a/optuna_dashboard/streamlit/_streamlit_helper.py b/optuna_dashboard/streamlit/_streamlit_helper.py index 719b5c0a..814a6327 100644 --- a/optuna_dashboard/streamlit/_streamlit_helper.py +++ b/optuna_dashboard/streamlit/_streamlit_helper.py @@ -66,7 +66,7 @@ def _render_widgets( ) elif widget["type"] == "text": # NOTE: Current implementation ignores "optional". - value = st.text_input(_format_description(widget["description"])) + value = st.text_input(_format_description(widget["description"])) # type: ignore elif widget["type"] == "user_attr": value = trial.user_attrs[widget["key"]] else: diff --git a/python_tests/streamlit/__init__.py b/python_tests/streamlit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python_tests/streamlit/test_streamlit_helper.py b/python_tests/streamlit/test_streamlit_helper.py new file mode 100644 index 00000000..3bd903d8 --- /dev/null +++ b/python_tests/streamlit/test_streamlit_helper.py @@ -0,0 +1,98 @@ +import itertools +from typing import Sequence + +import optuna +from optuna_dashboard import ChoiceWidget +from optuna_dashboard import register_objective_form_widgets +from optuna_dashboard import register_user_attr_form_widgets +from optuna_dashboard import save_note +from optuna_dashboard import SliderWidget +from optuna_dashboard import TextInputWidget +from optuna_dashboard.streamlit import render_objective_form_widgets +from optuna_dashboard.streamlit import render_trial_note +from optuna_dashboard.streamlit import render_user_attr_form_widgets +import pytest + + +@pytest.mark.parametrize("note", ["test", ""]) +def test_render_trial_note(note: str) -> None: + study = optuna.create_study() + + trial = study.ask() + save_note(trial, note) + + render_trial_note(study, study.trials[0]) + + +def test_render_trial_note_without_note() -> None: + study = optuna.create_study() + + study.ask() + + render_trial_note(study, study.trials[0]) + + +widget_list_for_user_attr = [ + ChoiceWidget( + choices=["Good", "Bad"], + values=[1, -1], + description="description", + user_attr_key="choice", + ), + SliderWidget( + min=1, + max=5, + step=1, + labels=[(1, "Bad"), (5, "Good")], + description="description", + user_attr_key="slider", + ), + TextInputWidget(description="description", user_attr_key="text1"), + TextInputWidget(description="description", user_attr_key="text2"), +] + +widgets_combinations_for_user_attr = [] +# Test widget combinations. +for r in range(len(widget_list_for_user_attr) + 1): + widgets_combinations_for_user_attr += list( + itertools.combinations(widget_list_for_user_attr, r) + ) + + +@pytest.mark.parametrize("widgets", widgets_combinations_for_user_attr) +def test_render_user_attr_form_widgets( + widgets: Sequence[ChoiceWidget | SliderWidget | TextInputWidget], +) -> None: + study = optuna.create_study() + register_user_attr_form_widgets(study, widgets) # type: ignore + + study.ask() + render_user_attr_form_widgets(study, study.trials[0]) + + +widget_list_for_objective = [ + ChoiceWidget( + choices=["Good", "Bad"], + values=[1, -1], + description="description", + ), + SliderWidget( + min=1, + max=5, + step=1, + labels=[(1, "Bad"), (5, "Good")], + description="description", + ), + TextInputWidget(description="description"), +] + + +@pytest.mark.parametrize("widget", widget_list_for_objective) +def test_render_objective_form_widgets( + widget: ChoiceWidget | SliderWidget | TextInputWidget, +) -> None: + study = optuna.create_study() + register_objective_form_widgets(study, [widget]) # type: ignore + + study.ask() + render_objective_form_widgets(study, study.trials[0]) From e8cc246580b12f508940022d5aba7c1e47eaf7fa Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 7 Jul 2023 12:39:39 +0900 Subject: [PATCH 31/39] Apply isort --- optuna_dashboard/streamlit/_streamlit_helper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/optuna_dashboard/streamlit/_streamlit_helper.py b/optuna_dashboard/streamlit/_streamlit_helper.py index 814a6327..e6e92307 100644 --- a/optuna_dashboard/streamlit/_streamlit_helper.py +++ b/optuna_dashboard/streamlit/_streamlit_helper.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING import optuna from optuna.trial import FrozenTrial + import streamlit as st from .._form_widget import get_form_widgets_json From 842f65b51b98c3000d3e85766763fc5f9449c422 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 7 Jul 2023 13:13:54 +0900 Subject: [PATCH 32/39] Add streamlit for test --- .github/workflows/python-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 2a8b3d97..cdfc573d 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -45,7 +45,7 @@ jobs: # python_tests requires optuna>=3.0.0 since it imports FloatDistribution run: | python -m pip install --progress-bar off --upgrade pip setuptools - pip install boto3 moto[s3] pytest + pip install streamlit boto3 moto[s3] pytest pip install --progress-bar off "optuna>=3.0.0" pip install --progress-bar off . - run: pytest python_tests From 4d728e45335073fc6772e7cf9beea8cbbac99fef Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 7 Jul 2023 15:55:32 +0900 Subject: [PATCH 33/39] Update test for combination --- .../streamlit/test_streamlit_helper.py | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/python_tests/streamlit/test_streamlit_helper.py b/python_tests/streamlit/test_streamlit_helper.py index 3bd903d8..a994c76e 100644 --- a/python_tests/streamlit/test_streamlit_helper.py +++ b/python_tests/streamlit/test_streamlit_helper.py @@ -32,7 +32,7 @@ def test_render_trial_note_without_note() -> None: render_trial_note(study, study.trials[0]) -widget_list_for_user_attr = [ +widget_list = [ ChoiceWidget( choices=["Good", "Bad"], values=[1, -1], @@ -53,10 +53,8 @@ widget_list_for_user_attr = [ widgets_combinations_for_user_attr = [] # Test widget combinations. -for r in range(len(widget_list_for_user_attr) + 1): - widgets_combinations_for_user_attr += list( - itertools.combinations(widget_list_for_user_attr, r) - ) +for r in range(len(widget_list) + 1): + widgets_combinations_for_user_attr += list(itertools.combinations(widget_list, r)) @pytest.mark.parametrize("widgets", widgets_combinations_for_user_attr) @@ -70,29 +68,18 @@ def test_render_user_attr_form_widgets( render_user_attr_form_widgets(study, study.trials[0]) -widget_list_for_objective = [ - ChoiceWidget( - choices=["Good", "Bad"], - values=[1, -1], - description="description", - ), - SliderWidget( - min=1, - max=5, - step=1, - labels=[(1, "Bad"), (5, "Good")], - description="description", - ), - TextInputWidget(description="description"), -] +widgets_combinations_for_objective = [] +# Test widget combinations. +for r in range(1, len(widget_list) + 1): + widgets_combinations_for_objective += list(itertools.combinations(widget_list, r)) -@pytest.mark.parametrize("widget", widget_list_for_objective) +@pytest.mark.parametrize("widgets", widgets_combinations_for_objective) def test_render_objective_form_widgets( - widget: ChoiceWidget | SliderWidget | TextInputWidget, + widgets: Sequence[ChoiceWidget | SliderWidget | TextInputWidget], ) -> None: - study = optuna.create_study() - register_objective_form_widgets(study, [widget]) # type: ignore + study = optuna.create_study(directions=["maximize"] * len(widgets)) + register_objective_form_widgets(study, widgets) # type: ignore study.ask() render_objective_form_widgets(study, study.trials[0]) From 853592755d9881571807f0f8ebed15efce132dd9 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 7 Jul 2023 16:09:50 +0900 Subject: [PATCH 34/39] Add callback function when feedback submission is completed by success --- .../streamlit/_streamlit_helper.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/optuna_dashboard/streamlit/_streamlit_helper.py b/optuna_dashboard/streamlit/_streamlit_helper.py index e6e92307..92f798c9 100644 --- a/optuna_dashboard/streamlit/_streamlit_helper.py +++ b/optuna_dashboard/streamlit/_streamlit_helper.py @@ -12,6 +12,8 @@ from .._note import get_note_from_system_attrs if TYPE_CHECKING: + from typing import Callable + from typing import Optional from typing import Sequence from .._form_widget import ChoiceWidgetJSON @@ -36,15 +38,15 @@ def _format_choice(choice: float, widget: ChoiceWidgetJSON) -> str: return widget["choices"][widget["values"].index(choice)] -def _format_description(description: str | None) -> str: +def _format_description(description: Optional[str]) -> str: return "" if description is None else description def _render_widgets( widgets: Sequence[ChoiceWidgetJSON | SliderWidgetJSON | TextInputWidgetJSON | UserAttrRefJSON], trial: FrozenTrial, -) -> tuple[bool, list[str | float | None]]: - values: list[str | float | None] = [] +) -> tuple[bool, list[Optional[str | float]]]: + values: list[Optional[str | float]] = [] with st.form("user_input", clear_on_submit=False): for widget in widgets: @@ -80,7 +82,9 @@ def _render_widgets( return submitted, values -def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> None: +def render_user_attr_form_widgets( + study: optuna.Study, trial: FrozenTrial, on_success_callback: Optional[Callable[[], None]] +) -> None: """Render user input widgets to UI with streamlit. Submitted values to the forms are registered as each trial's user_attrs. @@ -88,6 +92,8 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No Args: study: The optuna study object to get widget specification. trial: The optuna trial object to save user feedbacks. + on_success_callback: The callback function which will be executed + when feedback submission is succeeded. Raises: ValueError: If No form widgets registered. @@ -110,10 +116,15 @@ def render_user_attr_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No trial._trial_id, key=widget["user_attr_key"], value=value # type: ignore ) - st.success("Submitted!") + if on_success_callback is None: + st.success("Submitted!") + else: + on_success_callback() -def render_objective_form_widgets(study: optuna.Study, trial: FrozenTrial) -> None: +def render_objective_form_widgets( + study: optuna.Study, trial: FrozenTrial, on_success_callback: Optional[Callable[[], None]] +) -> None: """Render user input widgets to UI with streamlit. Submitted values to the forms are telled to optuna trial object. @@ -123,6 +134,8 @@ def render_objective_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No Args: study: The optuna study object to get widget specification. trial: The optuna trial object to tell user feedbacks. + on_success_callback: The callback function which will be executed + when feedback submission is succeeded. Raises: ValueError: If No form widgets registered. @@ -147,4 +160,8 @@ def render_objective_form_widgets(study: optuna.Study, trial: FrozenTrial) -> No raise ValueError("All submitted values should be float.") from e study.tell(trial.number, values_float) - st.success("Submitted!") + + if on_success_callback is None: + st.success("Submitted!") + else: + on_success_callback() From 1e37671fb2c500b03f1e85ca91ee24772e2af973 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 7 Jul 2023 16:22:40 +0900 Subject: [PATCH 35/39] Change error handling --- .../streamlit/_streamlit_helper.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/optuna_dashboard/streamlit/_streamlit_helper.py b/optuna_dashboard/streamlit/_streamlit_helper.py index 92f798c9..14f746fd 100644 --- a/optuna_dashboard/streamlit/_streamlit_helper.py +++ b/optuna_dashboard/streamlit/_streamlit_helper.py @@ -153,15 +153,15 @@ def render_objective_form_widgets( if submitted: values_float = [] - for value in values: - try: + try: + for value in values: values_float.append(float(value)) # type: ignore - except ValueError as e: - raise ValueError("All submitted values should be float.") from e - study.tell(trial.number, values_float) + study.tell(trial.number, values_float) - if on_success_callback is None: - st.success("Submitted!") - else: - on_success_callback() + if on_success_callback is None: + st.success("Submitted!") + else: + on_success_callback() + except ValueError: + st.error("Please enter float values.") From 294c0657df2f2dd9a22b55363c7f532c872cbd2a Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 7 Jul 2023 16:28:05 +0900 Subject: [PATCH 36/39] Add default value for on_success_callback --- optuna_dashboard/streamlit/_streamlit_helper.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/streamlit/_streamlit_helper.py b/optuna_dashboard/streamlit/_streamlit_helper.py index 14f746fd..252c0901 100644 --- a/optuna_dashboard/streamlit/_streamlit_helper.py +++ b/optuna_dashboard/streamlit/_streamlit_helper.py @@ -83,7 +83,9 @@ def _render_widgets( def render_user_attr_form_widgets( - study: optuna.Study, trial: FrozenTrial, on_success_callback: Optional[Callable[[], None]] + study: optuna.Study, + trial: FrozenTrial, + on_success_callback: Optional[Callable[[], None]] = None, ) -> None: """Render user input widgets to UI with streamlit. @@ -123,7 +125,9 @@ def render_user_attr_form_widgets( def render_objective_form_widgets( - study: optuna.Study, trial: FrozenTrial, on_success_callback: Optional[Callable[[], None]] + study: optuna.Study, + trial: FrozenTrial, + on_success_callback: Optional[Callable[[], None]] = None, ) -> None: """Render user input widgets to UI with streamlit. From 244f1c83253dc486f58b80c51b8b22b73d861884 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 7 Jul 2023 16:38:13 +0900 Subject: [PATCH 37/39] Change type annotation style --- optuna_dashboard/streamlit/_streamlit_helper.py | 9 ++++++--- python_tests/streamlit/test_streamlit_helper.py | 5 +++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/optuna_dashboard/streamlit/_streamlit_helper.py b/optuna_dashboard/streamlit/_streamlit_helper.py index 252c0901..45a29ee7 100644 --- a/optuna_dashboard/streamlit/_streamlit_helper.py +++ b/optuna_dashboard/streamlit/_streamlit_helper.py @@ -15,6 +15,7 @@ if TYPE_CHECKING: from typing import Callable from typing import Optional from typing import Sequence + from typing import Union from .._form_widget import ChoiceWidgetJSON from .._form_widget import SliderWidgetJSON @@ -43,10 +44,12 @@ def _format_description(description: Optional[str]) -> str: def _render_widgets( - widgets: Sequence[ChoiceWidgetJSON | SliderWidgetJSON | TextInputWidgetJSON | UserAttrRefJSON], + widgets: Sequence[ + Union[ChoiceWidgetJSON, SliderWidgetJSON, TextInputWidgetJSON, UserAttrRefJSON] + ], trial: FrozenTrial, -) -> tuple[bool, list[Optional[str | float]]]: - values: list[Optional[str | float]] = [] +) -> tuple[bool, list[Optional[Union[str, float]]]]: + values: list[Optional[Union[str, float]]] = [] with st.form("user_input", clear_on_submit=False): for widget in widgets: diff --git a/python_tests/streamlit/test_streamlit_helper.py b/python_tests/streamlit/test_streamlit_helper.py index a994c76e..8f324820 100644 --- a/python_tests/streamlit/test_streamlit_helper.py +++ b/python_tests/streamlit/test_streamlit_helper.py @@ -1,5 +1,6 @@ import itertools from typing import Sequence +from typing import Union import optuna from optuna_dashboard import ChoiceWidget @@ -59,7 +60,7 @@ for r in range(len(widget_list) + 1): @pytest.mark.parametrize("widgets", widgets_combinations_for_user_attr) def test_render_user_attr_form_widgets( - widgets: Sequence[ChoiceWidget | SliderWidget | TextInputWidget], + widgets: Sequence[Union[ChoiceWidget, SliderWidget, TextInputWidget]], ) -> None: study = optuna.create_study() register_user_attr_form_widgets(study, widgets) # type: ignore @@ -76,7 +77,7 @@ for r in range(1, len(widget_list) + 1): @pytest.mark.parametrize("widgets", widgets_combinations_for_objective) def test_render_objective_form_widgets( - widgets: Sequence[ChoiceWidget | SliderWidget | TextInputWidget], + widgets: Sequence[Union[ChoiceWidget, SliderWidget, TextInputWidget]], ) -> None: study = optuna.create_study(directions=["maximize"] * len(widgets)) register_objective_form_widgets(study, widgets) # type: ignore From e9fb86907a531c3603144fccd1b3940e175860b9 Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 7 Jul 2023 16:47:37 +0900 Subject: [PATCH 38/39] Add streamlit to CI --- .github/workflows/python-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index cdfc573d..612ddb59 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -61,7 +61,7 @@ jobs: - name: Install dependencies run: | python -m pip install --progress-bar off --upgrade pip setuptools - pip install boto3 moto[s3] pytest + pip install streamlit boto3 moto[s3] pytest pip install --progress-bar off . python -m pip install --progress-bar off --upgrade git+https://github.com/optuna/optuna.git - run: pytest python_tests From fad5c7905e144bd481a8ebadbb85775734012e3a Mon Sep 17 00:00:00 2001 From: cross32768 Date: Fri, 7 Jul 2023 17:12:45 +0900 Subject: [PATCH 39/39] Add key --- optuna_dashboard/streamlit/_streamlit_helper.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/streamlit/_streamlit_helper.py b/optuna_dashboard/streamlit/_streamlit_helper.py index 45a29ee7..05744860 100644 --- a/optuna_dashboard/streamlit/_streamlit_helper.py +++ b/optuna_dashboard/streamlit/_streamlit_helper.py @@ -52,7 +52,7 @@ def _render_widgets( values: list[Optional[Union[str, float]]] = [] with st.form("user_input", clear_on_submit=False): - for widget in widgets: + for i, widget in enumerate(widgets): if widget["type"] == "choice": value = st.radio( _format_description(widget["description"]), @@ -61,6 +61,7 @@ def _render_widgets( choice, widget ), horizontal=True, + key=f"radio_{i}", ) elif widget["type"] == "slider": # NOTE: It is difficult to reflect "labels". @@ -69,10 +70,13 @@ def _render_widgets( min_value=widget["min"], max_value=widget["max"], step=widget["step"], + key=f"slider_{i}", ) elif widget["type"] == "text": # NOTE: Current implementation ignores "optional". - value = st.text_input(_format_description(widget["description"])) # type: ignore + value = st.text_input( + _format_description(widget["description"]), key=f"text_{i}" + ) # type: ignore elif widget["type"] == "user_attr": value = trial.user_attrs[widget["key"]] else: