mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-25 13:50:21 +08:00
Merge pull request #431 from c-bata/follow-up-411
Follow-up #411 to refactor type definitions
This commit is contained in:
+3
-3
@@ -23,9 +23,9 @@ Human-in-the-loop
|
||||
:nosignatures:
|
||||
|
||||
optuna_dashboard.register_objective_form_widgets
|
||||
optuna_dashboard.ObjectiveChoiceWidget
|
||||
optuna_dashboard.ObjectiveSliderWidget
|
||||
optuna_dashboard.ObjectiveTextInputWidget
|
||||
optuna_dashboard.ChoiceWidget
|
||||
optuna_dashboard.SliderWidget
|
||||
optuna_dashboard.TextInputWidget
|
||||
optuna_dashboard.ObjectiveUserAttrRef
|
||||
|
||||
Artifact
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
from ._app import run_server # noqa
|
||||
from ._app import wsgi # noqa
|
||||
from ._form_widget import ChoiceWidget # noqa
|
||||
from ._form_widget import ObjectiveChoiceWidget # noqa
|
||||
from ._form_widget import ObjectiveSliderWidget # noqa
|
||||
from ._form_widget import ObjectiveTextInputWidget # noqa
|
||||
from ._form_widget import ObjectiveUserAttrRef # noqa
|
||||
from ._form_widget import register_objective_form_widgets # noqa
|
||||
from ._form_widget import register_user_attr_form_widgets # noqa
|
||||
from ._form_widget import SliderWidget # noqa
|
||||
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 ._objective_form_widget import ObjectiveChoiceWidget # noqa
|
||||
from ._objective_form_widget import ObjectiveSliderWidget # noqa
|
||||
from ._objective_form_widget import ObjectiveTextInputWidget # noqa
|
||||
from ._objective_form_widget import ObjectiveUserAttrRef # noqa
|
||||
from ._objective_form_widget import register_objective_form_widgets # noqa
|
||||
|
||||
|
||||
__version__ = "0.9.0"
|
||||
|
||||
@@ -447,6 +447,24 @@ def create_app(
|
||||
response.status = 204
|
||||
return {}
|
||||
|
||||
@app.post("/api/trials/<trial_id:int>/user-attrs")
|
||||
@json_api_view
|
||||
def save_trial_user_attrs(trial_id: int) -> dict[str, Any]:
|
||||
user_attrs = request.json.get("user_attrs", {})
|
||||
if not user_attrs:
|
||||
response.status = 400 # Bad request
|
||||
return {"reason": "user_attrs must be specified."}
|
||||
|
||||
try:
|
||||
for key, val in user_attrs.items():
|
||||
storage.set_trial_user_attr(trial_id, key, val)
|
||||
except Exception as e:
|
||||
response.status = 500
|
||||
return {"reason": f"Internal server error: {e}"}
|
||||
|
||||
response.status = 204
|
||||
return {}
|
||||
|
||||
@app.put("/api/studies/<study_id:int>/<trial_id:int>/note")
|
||||
@json_api_view
|
||||
def save_trial_note(study_id: int, trial_id: int) -> dict[str, Any]:
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
import optuna
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from typing import Literal
|
||||
from typing import Optional
|
||||
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]},
|
||||
)
|
||||
UserAttrRefJSON = TypedDict(
|
||||
"UserAttrRefJSON",
|
||||
{"type": Literal["user_attr"], "key": str, "user_attr_key": Optional[str]},
|
||||
)
|
||||
FormWidgetJSON = TypedDict(
|
||||
"FormWidgetJSON",
|
||||
{
|
||||
"output_type": Literal["objective", "user_attr"],
|
||||
"widgets": list[
|
||||
Union[ChoiceWidgetJSON, SliderWidgetJSON, TextInputWidgetJSON, UserAttrRefJSON]
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChoiceWidget:
|
||||
choices: list[str]
|
||||
values: list[float]
|
||||
description: Optional[str] = None
|
||||
user_attr_key: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> ChoiceWidgetJSON:
|
||||
return {
|
||||
"type": "choice",
|
||||
"description": self.description,
|
||||
"choices": self.choices,
|
||||
"values": self.values,
|
||||
"user_attr_key": self.user_attr_key,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class SliderWidget:
|
||||
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:
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextInputWidget:
|
||||
description: Optional[str] = None
|
||||
user_attr_key: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> TextInputWidgetJSON:
|
||||
return {
|
||||
"type": "text",
|
||||
"description": self.description,
|
||||
"user_attr_key": self.user_attr_key,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ObjectiveUserAttrRef:
|
||||
key: str
|
||||
user_attr_key: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> UserAttrRefJSON:
|
||||
return {
|
||||
"type": "user_attr",
|
||||
"key": self.key,
|
||||
"user_attr_key": self.user_attr_key,
|
||||
}
|
||||
|
||||
|
||||
ObjectiveFormWidget = Union[ChoiceWidget, SliderWidget, TextInputWidget, ObjectiveUserAttrRef]
|
||||
# For backward compatibility.
|
||||
ObjectiveChoiceWidget = ChoiceWidget
|
||||
ObjectiveSliderWidget = SliderWidget
|
||||
ObjectiveTextInputWidget = TextInputWidget
|
||||
FORM_WIDGETS_KEY = "dashboard:form_widgets:v2"
|
||||
|
||||
|
||||
def register_objective_form_widgets(
|
||||
study: optuna.Study, widgets: list[ObjectiveFormWidget]
|
||||
) -> None:
|
||||
if len(study.directions) != len(widgets):
|
||||
raise ValueError("The length of actions must be the same with the number of objectives.")
|
||||
if any(w.user_attr_key is not None for w in widgets):
|
||||
warnings.warn("`user_attr_key` specified, but it will not be used.")
|
||||
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, form_widgets)
|
||||
|
||||
|
||||
def register_user_attr_form_widgets(
|
||||
study: optuna.Study, widgets: list[ObjectiveFormWidget]
|
||||
) -> None:
|
||||
if any(w.user_attr_key is None for w in widgets):
|
||||
raise ValueError("`user_attr_key` is not specified.")
|
||||
if len(widgets) != len(set(w.user_attr_key for w in widgets)):
|
||||
raise ValueError("`user_attr_key` must be unique for each widget.")
|
||||
form_widgets: FormWidgetJSON = {
|
||||
"output_type": "user_attr",
|
||||
"widgets": [w.to_dict() for w in widgets],
|
||||
}
|
||||
study._storage.set_study_system_attr(study._study_id, FORM_WIDGETS_KEY, form_widgets)
|
||||
|
||||
|
||||
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
|
||||
@@ -1,133 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import optuna
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from typing import Literal
|
||||
from typing import Optional
|
||||
from typing import TypedDict
|
||||
|
||||
ChoiceWidgetJSON = TypedDict(
|
||||
"ChoiceWidgetJSON",
|
||||
{
|
||||
"type": Literal["choice"],
|
||||
"description": Optional[str],
|
||||
"choices": list[str],
|
||||
"values": list[float],
|
||||
},
|
||||
)
|
||||
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]],
|
||||
},
|
||||
)
|
||||
TextInputWidgetJSON = TypedDict(
|
||||
"TextInputWidgetJSON",
|
||||
{"type": Literal["text"], "description": Optional[str]},
|
||||
)
|
||||
UserAttrRefJSON = TypedDict("UserAttrRefJSON", {"type": Literal["user_attr"], "key": str})
|
||||
ObjectiveFormWidgetJSON = Union[
|
||||
ChoiceWidgetJSON, SliderWidgetJSON, TextInputWidgetJSON, UserAttrRefJSON
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ObjectiveChoiceWidget:
|
||||
choices: list[str]
|
||||
values: list[float]
|
||||
description: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> ChoiceWidgetJSON:
|
||||
return {
|
||||
"type": "choice",
|
||||
"description": self.description,
|
||||
"choices": self.choices,
|
||||
"values": self.values,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ObjectiveSliderWidget:
|
||||
min: float
|
||||
max: float
|
||||
step: Optional[float] = None
|
||||
labels: Optional[list[tuple[float, str]]] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> SliderWidgetJSON:
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ObjectiveTextInputWidget:
|
||||
description: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> TextInputWidgetJSON:
|
||||
return {
|
||||
"type": "text",
|
||||
"description": self.description,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ObjectiveUserAttrRef:
|
||||
key: str
|
||||
|
||||
def to_dict(self) -> UserAttrRefJSON:
|
||||
return {
|
||||
"type": "user_attr",
|
||||
"key": self.key,
|
||||
}
|
||||
|
||||
|
||||
ObjectiveFormWidget = Union[
|
||||
ObjectiveChoiceWidget, ObjectiveSliderWidget, ObjectiveTextInputWidget, ObjectiveUserAttrRef
|
||||
]
|
||||
SYSTEM_ATTR_KEY = "dashboard:objective_form_widgets:v1"
|
||||
|
||||
|
||||
def register_objective_form_widgets(
|
||||
study: optuna.Study, widgets: list[ObjectiveFormWidget]
|
||||
) -> None:
|
||||
if len(study.directions) != len(widgets):
|
||||
raise ValueError("The length of actions must be the same with the number of objectives.")
|
||||
widget_dicts = [w.to_dict() for w in widgets]
|
||||
study._storage.set_study_system_attr(study._study_id, SYSTEM_ATTR_KEY, widget_dicts)
|
||||
|
||||
|
||||
def get_objective_form_widgets_json(
|
||||
study_system_attr: dict[str, Any]
|
||||
) -> Optional[list[ObjectiveFormWidgetJSON]]:
|
||||
if SYSTEM_ATTR_KEY in study_system_attr:
|
||||
return study_system_attr[SYSTEM_ATTR_KEY]
|
||||
# For optuna-dashboard v0.9.0b5 users
|
||||
if "dashboard:objective_form_widgets" in study_system_attr:
|
||||
return json.loads(study_system_attr["dashboard:objective_form_widgets"])
|
||||
return None
|
||||
@@ -12,8 +12,8 @@ from optuna.study import StudySummary
|
||||
from optuna.trial import FrozenTrial
|
||||
|
||||
from . import _note as note
|
||||
from ._form_widget import get_form_widgets_json
|
||||
from ._named_objectives import get_objective_names
|
||||
from ._objective_form_widget import get_objective_form_widgets_json
|
||||
from .artifact._backend import list_trial_artifacts
|
||||
|
||||
|
||||
@@ -144,9 +144,9 @@ def serialize_study_detail(
|
||||
objective_names = get_objective_names(system_attrs)
|
||||
if objective_names:
|
||||
serialized["objective_names"] = objective_names
|
||||
objective_form_widgets = get_objective_form_widgets_json(system_attrs)
|
||||
if objective_form_widgets:
|
||||
serialized["objective_form_widgets"] = objective_form_widgets
|
||||
form_widgets = get_form_widgets_json(system_attrs)
|
||||
if form_widgets:
|
||||
serialized["form_widgets"] = form_widgets
|
||||
return serialized
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
saveStudyNoteAPI,
|
||||
saveTrialNoteAPI,
|
||||
tellTrialAPI,
|
||||
saveTrialUserAttrsAPI,
|
||||
renameStudyAPI,
|
||||
uploadArtifactAPI,
|
||||
getMetaInfoAPI,
|
||||
@@ -151,6 +152,26 @@ export const actionCreator = () => {
|
||||
setStudyDetailState(studyId, newStudy)
|
||||
}
|
||||
|
||||
const setTrialUserAttrs = (
|
||||
studyId: number,
|
||||
index: number,
|
||||
user_attrs: { [key: string]: number }
|
||||
) => {
|
||||
const newTrial: Trial = Object.assign(
|
||||
{},
|
||||
studyDetails[studyId].trials[index]
|
||||
)
|
||||
newTrial.user_attrs = Object.keys(user_attrs).map((key) => ({
|
||||
key: key,
|
||||
value: user_attrs[key].toString(),
|
||||
}))
|
||||
const newTrials: Trial[] = [...studyDetails[studyId].trials]
|
||||
newTrials[index] = newTrial
|
||||
const newStudy: StudyDetail = Object.assign({}, studyDetails[studyId])
|
||||
newStudy.trials = newTrials
|
||||
setStudyDetailState(studyId, newStudy)
|
||||
}
|
||||
|
||||
const setStudyParamImportanceState = (
|
||||
studyId: number,
|
||||
importance: ParamImportance[][]
|
||||
@@ -505,6 +526,40 @@ export const actionCreator = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const saveTrialUserAttrs = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
user_attrs: { [key: string]: number }
|
||||
): void => {
|
||||
console.log("user_attrs", user_attrs)
|
||||
const message = `id=${trialId}, user_attrs=${JSON.stringify(user_attrs)}`
|
||||
saveTrialUserAttrsAPI(trialId, user_attrs)
|
||||
.then(() => {
|
||||
const index = studyDetails[studyId].trials.findIndex(
|
||||
(t) => t.trial_id === trialId
|
||||
)
|
||||
if (index === -1) {
|
||||
enqueueSnackbar(`Unexpected error happens. Please reload the page.`, {
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
setTrialUserAttrs(studyId, index, user_attrs)
|
||||
enqueueSnackbar(`Successfully updated trial (${message})`, {
|
||||
variant: "success",
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
const reason = err.response?.data.reason
|
||||
enqueueSnackbar(
|
||||
`Failed to update trial (${message}). Reason: ${reason}`,
|
||||
{
|
||||
variant: "error",
|
||||
}
|
||||
)
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
return {
|
||||
updateAPIMeta,
|
||||
updateStudyDetail,
|
||||
@@ -523,6 +578,7 @@ export const actionCreator = () => {
|
||||
deleteArtifact,
|
||||
makeTrialComplete,
|
||||
makeTrialFail,
|
||||
saveTrialUserAttrs,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ interface StudyDetailResponse {
|
||||
has_intermediate_values: boolean
|
||||
note: Note
|
||||
objective_names?: string[]
|
||||
objective_form_widgets?: ObjectiveFormWidget[]
|
||||
form_widgets?: FormWidgets
|
||||
}
|
||||
|
||||
export const getStudyDetailAPI = (
|
||||
@@ -100,7 +100,7 @@ export const getStudyDetailAPI = (
|
||||
has_intermediate_values: res.data.has_intermediate_values,
|
||||
note: res.data.note,
|
||||
objective_names: res.data.objective_names,
|
||||
objective_form_widgets: res.data.objective_form_widgets,
|
||||
form_widgets: res.data.form_widgets,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -281,6 +281,19 @@ export const tellTrialAPI = (
|
||||
})
|
||||
}
|
||||
|
||||
export const saveTrialUserAttrsAPI = (
|
||||
trialId: number,
|
||||
user_attrs: { [key: string]: number }
|
||||
): Promise<void> => {
|
||||
const req = { user_attrs: user_attrs }
|
||||
|
||||
return axiosInstance
|
||||
.post<void>(`/api/trials/${trialId}/user-attrs`, req)
|
||||
.then((res) => {
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
interface ParamImportancesResponse {
|
||||
param_importances: ParamImportance[][]
|
||||
}
|
||||
|
||||
@@ -20,13 +20,12 @@ export const ObjectiveForm: FC<{
|
||||
trial: Trial
|
||||
directions: StudyDirection[]
|
||||
names: string[]
|
||||
widgets: ObjectiveFormWidget[]
|
||||
}> = ({ trial, directions, names, widgets }) => {
|
||||
formWidgets: FormWidgets
|
||||
}> = ({ trial, directions, names, formWidgets }) => {
|
||||
const theme = useTheme()
|
||||
const action = actionCreator()
|
||||
const [values, setValues] = useState<(number | null)[]>(
|
||||
directions.map((d, i) => {
|
||||
const widget = widgets.at(i)
|
||||
formWidgets.widgets.map((widget) => {
|
||||
if (widget === undefined) {
|
||||
return null
|
||||
} else if (widget.type === "text") {
|
||||
@@ -65,23 +64,38 @@ export const ObjectiveForm: FC<{
|
||||
|
||||
const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
e.preventDefault()
|
||||
const filtered = values.filter<number>((v): v is number => v !== null)
|
||||
if (filtered.length !== directions.length) {
|
||||
return
|
||||
if (formWidgets.output_type == "objective") {
|
||||
const filtered = values.filter<number>((v): v is number => v !== null)
|
||||
if (filtered.length !== directions.length) {
|
||||
return
|
||||
}
|
||||
action.makeTrialComplete(trial.study_id, trial.trial_id, filtered)
|
||||
} else if (formWidgets.output_type == "user_attr") {
|
||||
const user_attrs = Object.fromEntries(
|
||||
formWidgets.widgets.map((widget, i) => [
|
||||
widget.user_attr_key,
|
||||
values[i],
|
||||
])
|
||||
)
|
||||
action.saveTrialUserAttrs(trial.study_id, trial.trial_id, user_attrs)
|
||||
}
|
||||
action.makeTrialComplete(trial.study_id, trial.trial_id, filtered)
|
||||
}
|
||||
|
||||
const getObjectiveName = (i: number): string => {
|
||||
const n = names.at(i)
|
||||
if (n !== undefined) {
|
||||
return n
|
||||
}
|
||||
if (directions.length == 1) {
|
||||
return `Objective`
|
||||
} else {
|
||||
return `Objective ${i}`
|
||||
const getMetricName = (i: number): string => {
|
||||
if (formWidgets.output_type == "objective") {
|
||||
const n = names.at(i)
|
||||
if (n !== undefined) {
|
||||
return n
|
||||
}
|
||||
if (directions.length == 1) {
|
||||
return `Objective`
|
||||
} else {
|
||||
return `Objective ${i}`
|
||||
}
|
||||
} else if (formWidgets.output_type == "user_attr") {
|
||||
return formWidgets.widgets[i].user_attr_key as string
|
||||
}
|
||||
return "Unkown metric name"
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -102,44 +116,14 @@ export const ObjectiveForm: FC<{
|
||||
p: theme.spacing(1),
|
||||
}}
|
||||
>
|
||||
{directions.map((d, i) => {
|
||||
const widget = widgets.at(i)
|
||||
{formWidgets.widgets.map((widget, i) => {
|
||||
const value = values.at(i)
|
||||
const key = `objective-${i}`
|
||||
if (widget === undefined) {
|
||||
return (
|
||||
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
|
||||
<FormLabel>{getObjectiveName(i)}</FormLabel>
|
||||
<DebouncedInputTextField
|
||||
onChange={(s, valid) => {
|
||||
const n = Number(s)
|
||||
if (s.length > 0 && valid && !isNaN(n)) {
|
||||
setValue(i, n)
|
||||
return
|
||||
} else if (values.at(i) !== null) {
|
||||
setValue(i, null)
|
||||
}
|
||||
}}
|
||||
delay={500}
|
||||
textFieldProps={{
|
||||
required: true,
|
||||
autoFocus: true,
|
||||
fullWidth: true,
|
||||
helperText:
|
||||
value === null || value === undefined
|
||||
? `Please input the float number.`
|
||||
: "",
|
||||
label: getObjectiveName(i),
|
||||
type: "text",
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)
|
||||
} else if (widget.type === "text") {
|
||||
if (widget.type === "text") {
|
||||
return (
|
||||
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
|
||||
<FormLabel>
|
||||
{getObjectiveName(i)} - {widget.description}
|
||||
{getMetricName(i)} - {widget.description}
|
||||
</FormLabel>
|
||||
<DebouncedInputTextField
|
||||
onChange={(s, valid) => {
|
||||
@@ -172,7 +156,7 @@ export const ObjectiveForm: FC<{
|
||||
return (
|
||||
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
|
||||
<FormLabel>
|
||||
{getObjectiveName(i)} - {widget.description}
|
||||
{getMetricName(i)} - {widget.description}
|
||||
</FormLabel>
|
||||
<RadioGroup row defaultValue={widget.values.at(0)}>
|
||||
{widget.choices.map((c, j) => (
|
||||
@@ -202,7 +186,7 @@ export const ObjectiveForm: FC<{
|
||||
return (
|
||||
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
|
||||
<FormLabel>
|
||||
{getObjectiveName(i)} - {widget.description}
|
||||
{getMetricName(i)} - {widget.description}
|
||||
</FormLabel>
|
||||
<Box sx={{ padding: theme.spacing(0, 2) }}>
|
||||
<Slider
|
||||
@@ -227,7 +211,7 @@ export const ObjectiveForm: FC<{
|
||||
} else if (widget.type === "user_attr") {
|
||||
return (
|
||||
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
|
||||
<FormLabel>{getObjectiveName(i)}</FormLabel>
|
||||
<FormLabel>{getMetricName(i)}</FormLabel>
|
||||
<TextField
|
||||
inputProps={{ readOnly: true }}
|
||||
value={value || undefined}
|
||||
@@ -280,19 +264,24 @@ export const ReadonlyObjectiveForm: FC<{
|
||||
trial: Trial
|
||||
directions: StudyDirection[]
|
||||
names: string[]
|
||||
widgets: ObjectiveFormWidget[]
|
||||
}> = ({ trial, directions, names, widgets }) => {
|
||||
formWidgets: FormWidgets
|
||||
}> = ({ trial, directions, names, formWidgets }) => {
|
||||
const theme = useTheme()
|
||||
const getObjectiveName = (i: number): string => {
|
||||
const n = names.at(i)
|
||||
if (n !== undefined) {
|
||||
return n
|
||||
}
|
||||
if (directions.length == 1) {
|
||||
return `Objective`
|
||||
} else {
|
||||
return `Objective ${i}`
|
||||
const getMetricName = (i: number): string => {
|
||||
if (formWidgets.output_type == "objective") {
|
||||
const n = names.at(i)
|
||||
if (n !== undefined) {
|
||||
return n
|
||||
}
|
||||
if (directions.length == 1) {
|
||||
return `Objective`
|
||||
} else {
|
||||
return `Objective ${i}`
|
||||
}
|
||||
} else if (formWidgets.output_type == "user_attr") {
|
||||
return formWidgets.widgets[i].user_attr_key as string
|
||||
}
|
||||
return "Unkown metric name"
|
||||
}
|
||||
return (
|
||||
<>
|
||||
@@ -312,24 +301,13 @@ export const ReadonlyObjectiveForm: FC<{
|
||||
p: theme.spacing(1),
|
||||
}}
|
||||
>
|
||||
{directions.map((d, i) => {
|
||||
const widget = widgets.at(i)
|
||||
{formWidgets.widgets.map((widget, i) => {
|
||||
const key = `objective-${i}`
|
||||
if (widget === undefined) {
|
||||
return (
|
||||
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
|
||||
<FormLabel>{getObjectiveName(i)}</FormLabel>
|
||||
<TextField
|
||||
inputProps={{ readOnly: true }}
|
||||
value={trial.values?.at(i)}
|
||||
/>
|
||||
</FormControl>
|
||||
)
|
||||
} else if (widget.type === "text") {
|
||||
if (widget.type === "text") {
|
||||
return (
|
||||
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
|
||||
<FormLabel>
|
||||
{getObjectiveName(i)} - {widget.description}
|
||||
{getMetricName(i)} - {widget.description}
|
||||
</FormLabel>
|
||||
<TextField
|
||||
inputProps={{ readOnly: true }}
|
||||
@@ -341,7 +319,7 @@ export const ReadonlyObjectiveForm: FC<{
|
||||
return (
|
||||
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
|
||||
<FormLabel>
|
||||
{getObjectiveName(i)} - {widget.description}
|
||||
{getMetricName(i)} - {widget.description}
|
||||
</FormLabel>
|
||||
<RadioGroup row defaultValue={trial.values?.at(i)}>
|
||||
{widget.choices.map((c, j) => (
|
||||
@@ -366,7 +344,7 @@ export const ReadonlyObjectiveForm: FC<{
|
||||
return (
|
||||
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
|
||||
<FormLabel>
|
||||
{getObjectiveName(i)} - {widget.description}
|
||||
{getMetricName(i)} - {widget.description}
|
||||
</FormLabel>
|
||||
<Box sx={{ padding: theme.spacing(0, 2) }}>
|
||||
<Slider
|
||||
@@ -390,7 +368,7 @@ export const ReadonlyObjectiveForm: FC<{
|
||||
} else if (widget.type === "user_attr") {
|
||||
return (
|
||||
<FormControl key={key} sx={{ margin: theme.spacing(1, 2) }}>
|
||||
<FormLabel>{getObjectiveName(i)}</FormLabel>
|
||||
<FormLabel>{getMetricName(i)}</FormLabel>
|
||||
<TextField
|
||||
inputProps={{ readOnly: true }}
|
||||
value={trial.values?.at(i)}
|
||||
|
||||
@@ -143,14 +143,8 @@ const TrialListDetail: FC<{
|
||||
isBestTrial: (trialId: number) => boolean
|
||||
directions: StudyDirection[]
|
||||
objectiveNames: string[]
|
||||
objectiveFormWidgets: ObjectiveFormWidget[]
|
||||
}> = ({
|
||||
trial,
|
||||
isBestTrial,
|
||||
directions,
|
||||
objectiveNames,
|
||||
objectiveFormWidgets,
|
||||
}) => {
|
||||
formWidgets?: FormWidgets
|
||||
}> = ({ trial, isBestTrial, directions, objectiveNames, formWidgets }) => {
|
||||
const theme = useTheme()
|
||||
const artifactEnabled = useRecoilValue<boolean>(artifactIsAvailable)
|
||||
const startMs = trial.datetime_start?.getTime()
|
||||
@@ -291,22 +285,26 @@ const TrialListDetail: FC<{
|
||||
latestNote={trial.note}
|
||||
cardSx={{ marginBottom: theme.spacing(2) }}
|
||||
/>
|
||||
{trial.state === "Running" && directions.length > 0 && (
|
||||
<ObjectiveForm
|
||||
trial={trial}
|
||||
directions={directions}
|
||||
names={objectiveNames}
|
||||
widgets={objectiveFormWidgets}
|
||||
/>
|
||||
)}
|
||||
{trial.state === "Complete" && directions.length > 0 && (
|
||||
<ReadonlyObjectiveForm
|
||||
trial={trial}
|
||||
directions={directions}
|
||||
names={objectiveNames}
|
||||
widgets={objectiveFormWidgets}
|
||||
/>
|
||||
)}
|
||||
{trial.state === "Running" &&
|
||||
directions.length > 0 &&
|
||||
formWidgets !== undefined && (
|
||||
<ObjectiveForm
|
||||
trial={trial}
|
||||
directions={directions}
|
||||
names={objectiveNames}
|
||||
formWidgets={formWidgets}
|
||||
/>
|
||||
)}
|
||||
{trial.state === "Complete" &&
|
||||
directions.length > 0 &&
|
||||
formWidgets !== undefined && (
|
||||
<ReadonlyObjectiveForm
|
||||
trial={trial}
|
||||
directions={directions}
|
||||
names={objectiveNames}
|
||||
formWidgets={formWidgets}
|
||||
/>
|
||||
)}
|
||||
{artifactEnabled && <TrialArtifact trial={trial} />}
|
||||
</Box>
|
||||
)
|
||||
@@ -813,9 +811,7 @@ export const TrialList: FC<{ studyDetail: StudyDetail | null }> = ({
|
||||
isBestTrial={isBestTrial}
|
||||
directions={studyDetail?.directions || []}
|
||||
objectiveNames={studyDetail?.objective_names || []}
|
||||
objectiveFormWidgets={
|
||||
studyDetail?.objective_form_widgets || []
|
||||
}
|
||||
formWidgets={studyDetail?.form_widgets}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
Vendored
+11
-1
@@ -128,6 +128,7 @@ type StudySummary = {
|
||||
type ObjectiveChoiceWidget = {
|
||||
type: "choice"
|
||||
description: string
|
||||
user_attr_key?: string
|
||||
choices: string[]
|
||||
values: number[]
|
||||
}
|
||||
@@ -135,6 +136,7 @@ type ObjectiveChoiceWidget = {
|
||||
type ObjectiveSliderWidget = {
|
||||
type: "slider"
|
||||
description: string
|
||||
user_attr_key?: string
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
@@ -149,19 +151,27 @@ type ObjectiveSliderWidget = {
|
||||
type ObjectiveTextInputWidget = {
|
||||
type: "text"
|
||||
description: string
|
||||
user_attr_key?: string
|
||||
}
|
||||
|
||||
type ObjectiveUserAttrRef = {
|
||||
type: "user_attr"
|
||||
key: string
|
||||
user_attr_key?: string
|
||||
}
|
||||
|
||||
// TODO(kenshin): Rename this type to FormWidget or something.
|
||||
type ObjectiveFormWidget =
|
||||
| ObjectiveChoiceWidget
|
||||
| ObjectiveSliderWidget
|
||||
| ObjectiveTextInputWidget
|
||||
| ObjectiveUserAttrRef
|
||||
|
||||
type FormWidgets = {
|
||||
output_type: "objective" | "user_attr"
|
||||
widgets: ObjectiveFormWidget[]
|
||||
}
|
||||
|
||||
type StudyDetail = {
|
||||
id: number
|
||||
name: string
|
||||
@@ -175,7 +185,7 @@ type StudyDetail = {
|
||||
has_intermediate_values: boolean
|
||||
note: Note
|
||||
objective_names?: string[]
|
||||
objective_form_widgets?: ObjectiveFormWidget[]
|
||||
form_widgets?: FormWidgets
|
||||
}
|
||||
|
||||
type StudyDetails = {
|
||||
|
||||
Reference in New Issue
Block a user