mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-20 13:00:32 +08:00
Merge branch 'main' into history-undo
This commit is contained in:
@@ -44,6 +44,7 @@ Preferential Optimization
|
||||
optuna_dashboard.preferential.create_study
|
||||
optuna_dashboard.preferential.load_study
|
||||
optuna_dashboard.preferential.PreferentialStudy
|
||||
optuna_dashboard.register_preference_feedback_component
|
||||
|
||||
Streamlit
|
||||
-----------------
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 ._preference_setting import register_preference_feedback_component # noqa
|
||||
|
||||
|
||||
__version__ = "0.13.0b1"
|
||||
|
||||
@@ -28,6 +28,7 @@ from ._cached_extra_study_property import get_cached_extra_study_property
|
||||
from ._custom_plot_data import get_plotly_graph_objects
|
||||
from ._importance import get_param_importance_from_trials_cache
|
||||
from ._pareto_front import get_pareto_front_trials
|
||||
from ._preference_setting import _register_preference_feedback_component
|
||||
from ._preferential_history import NewHistory
|
||||
from ._preferential_history import PreferenceHistoryNotFound
|
||||
from ._preferential_history import remove_history
|
||||
@@ -313,6 +314,28 @@ def create_app(
|
||||
response.status = 204
|
||||
return {}
|
||||
|
||||
@app.put("/api/studies/<study_id:int>/preference_feedback_component")
|
||||
@json_api_view
|
||||
def put_preference_feedback_component(study_id: int) -> dict[str, Any]:
|
||||
try:
|
||||
component_type = request.json.get("output_type", "")
|
||||
artifact_key = request.json.get("artifact_key", None)
|
||||
except ValueError:
|
||||
response.status = 400
|
||||
return {"reason": "invalid request."}
|
||||
if component_type not in ["note", "artifact"]:
|
||||
response.status = 400
|
||||
return {"reason": "component_type must be either 'note' or 'artifact'."}
|
||||
|
||||
_register_preference_feedback_component(
|
||||
study_id=study_id,
|
||||
storage=storage,
|
||||
component_type=component_type,
|
||||
artifact_key=artifact_key,
|
||||
)
|
||||
response.status = 204
|
||||
return {}
|
||||
|
||||
@app.delete("/api/studies/<study_id:int>/preference/<history_id>")
|
||||
@json_api_view
|
||||
def remove_preference(study_id: int, history_id: str) -> dict[str, Any]:
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from optuna.storages import BaseStorage
|
||||
|
||||
from .preferential._study import PreferentialStudy
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Literal
|
||||
|
||||
OUTPUT_COMPONENT_TYPE = Literal["note", "artifact"]
|
||||
|
||||
_SYSTEM_ATTR_FEEDBACK_COMPONENT = "preference:component"
|
||||
|
||||
|
||||
def _register_preference_feedback_component(
|
||||
study_id: int,
|
||||
storage: BaseStorage,
|
||||
component_type: OUTPUT_COMPONENT_TYPE,
|
||||
artifact_key: str | None = None,
|
||||
) -> None:
|
||||
value: dict[str, Any] = {"output_type": component_type}
|
||||
if artifact_key is not None:
|
||||
value["artifact_key"] = artifact_key
|
||||
storage.set_study_system_attr(
|
||||
study_id=study_id,
|
||||
key=_SYSTEM_ATTR_FEEDBACK_COMPONENT,
|
||||
value=value,
|
||||
)
|
||||
|
||||
|
||||
def register_preference_feedback_component(
|
||||
study: PreferentialStudy,
|
||||
component_type: OUTPUT_COMPONENT_TYPE,
|
||||
artifact_key: str | None = None,
|
||||
) -> None:
|
||||
"""Register a preference feedback component to the study.
|
||||
|
||||
With this feature, you can change the component, displayed on the
|
||||
human feedback pages. By default, the Markdown note (``component_type="note"``)
|
||||
is displayed. If you specify ``component_type="artifact"``, the viewer for the
|
||||
specified artifact file will be displayed.
|
||||
Args:
|
||||
study:
|
||||
The study to register the preference feedback component.
|
||||
component_type:
|
||||
The component type, displayed on the human feedback pages
|
||||
(default: ``"note"``).
|
||||
user_attr_artifact_key:
|
||||
This option is required when the ``component_type`` is ``"artifact"``.
|
||||
The user attribute, which is specified this field, must contain the
|
||||
``artifact``id you want to display on the human feedback page.
|
||||
"""
|
||||
if component_type == "artifact":
|
||||
assert (
|
||||
artifact_key is not None
|
||||
), "artifact_key must be specified when component_type is Artifact"
|
||||
|
||||
_register_preference_feedback_component(
|
||||
study_id=study._study._study_id,
|
||||
storage=study._study._storage,
|
||||
component_type=component_type,
|
||||
artifact_key=artifact_key,
|
||||
)
|
||||
@@ -15,6 +15,7 @@ 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 ._preference_setting import _SYSTEM_ATTR_FEEDBACK_COMPONENT
|
||||
from ._preferential_history import _SYSTEM_ATTR_PREFIX_HISTORY
|
||||
from .artifact._backend import list_trial_artifacts
|
||||
from .preferential._study import _SYSTEM_ATTR_PREFERENTIAL_STUDY
|
||||
@@ -164,6 +165,12 @@ def serialize_study_detail(
|
||||
form_widgets = get_form_widgets_json(system_attrs)
|
||||
if form_widgets:
|
||||
serialized["form_widgets"] = form_widgets
|
||||
serialized["feedback_component_type"] = system_attrs.get(
|
||||
_SYSTEM_ATTR_FEEDBACK_COMPONENT,
|
||||
{
|
||||
"output_type": "note",
|
||||
},
|
||||
)
|
||||
if serialized["is_preferential"]:
|
||||
serialized["preference_history"] = serialize_preference_history(system_attrs)
|
||||
serialized["preferences"] = get_preferences(system_attrs)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import math
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
|
||||
import botorch.acquisition.analytic
|
||||
import botorch.models.model
|
||||
@@ -14,6 +16,7 @@ from gpytorch.likelihoods.gaussian_likelihood import Prior
|
||||
import numpy as np
|
||||
import optuna
|
||||
import optuna._transform
|
||||
from optuna.distributions import CategoricalDistribution
|
||||
import torch
|
||||
from torch import Tensor
|
||||
|
||||
@@ -310,7 +313,7 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler):
|
||||
search_space: dict[str, optuna.distributions.BaseDistribution],
|
||||
) -> dict[str, Any]:
|
||||
preferences = get_preferences(study.system_attrs)
|
||||
if len(preferences) == 0:
|
||||
if len(preferences) == 0 or len(search_space) == 0:
|
||||
return {}
|
||||
|
||||
trials = study.get_trials(deepcopy=False)
|
||||
@@ -355,16 +358,33 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler):
|
||||
best_f=torch.max(sampled_gp.posterior(params[:, None, :]).mean),
|
||||
)
|
||||
|
||||
# TODO: Make it possible to apply it on categorical variables
|
||||
candidates, _ = botorch.optim.optimize_acqf(
|
||||
acq_function=acqf,
|
||||
bounds=torch.from_numpy(trans.bounds.T),
|
||||
q=1,
|
||||
num_restarts=10,
|
||||
raw_samples=512,
|
||||
options={"batch_limit": 5, "maxiter": 200},
|
||||
sequential=True,
|
||||
)
|
||||
# TODO: Make it possible to apply it on mixed search space
|
||||
if all(isinstance(dist, CategoricalDistribution) for dist in search_space.values()):
|
||||
all_param_combinations = itertools.product(
|
||||
*[
|
||||
[(name, choice) for choice in cast(CategoricalDistribution, dist).choices]
|
||||
for name, dist in search_space.items()
|
||||
]
|
||||
)
|
||||
choices = torch.tensor(
|
||||
np.array([trans.transform(dict(params)) for params in all_param_combinations]),
|
||||
dtype=torch.float64,
|
||||
)
|
||||
candidates, _ = botorch.optim.optimize_acqf_discrete(
|
||||
acq_function=acqf,
|
||||
choices=choices,
|
||||
q=1,
|
||||
)
|
||||
else:
|
||||
candidates, _ = botorch.optim.optimize_acqf(
|
||||
acq_function=acqf,
|
||||
bounds=torch.from_numpy(trans.bounds.T),
|
||||
q=1,
|
||||
num_restarts=10,
|
||||
raw_samples=512,
|
||||
options={"batch_limit": 5, "maxiter": 200},
|
||||
sequential=True,
|
||||
)
|
||||
next_x = trans.untransform(candidates[0].detach().numpy())
|
||||
return next_x
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
skipPreferentialTrialAPI,
|
||||
removePreferentialHistoryAPI,
|
||||
restorePreferentialHistoryAPI,
|
||||
reportFeedbackComponentAPI,
|
||||
} from "./apiClient"
|
||||
import {
|
||||
graphVisibilityState,
|
||||
@@ -610,6 +611,27 @@ export const actionCreator = () => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
const updateFeedbackComponent = (
|
||||
studyId: number,
|
||||
compoennt_type: FeedbackComponentType
|
||||
) => {
|
||||
reportFeedbackComponentAPI(studyId, compoennt_type)
|
||||
.then(() => {
|
||||
const newStudy = Object.assign({}, studyDetails[studyId])
|
||||
newStudy.feedback_component_type = compoennt_type
|
||||
setStudyDetailState(studyId, newStudy)
|
||||
})
|
||||
.catch((err) => {
|
||||
const reason = err.response?.data.reason
|
||||
enqueueSnackbar(
|
||||
`Failed to report feedback component. Reason: ${reason}`,
|
||||
{
|
||||
variant: "error",
|
||||
}
|
||||
)
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
const removePreferentialHistory = (studyId: number, historyId: string) => {
|
||||
removePreferentialHistoryAPI(studyId, historyId)
|
||||
@@ -622,6 +644,7 @@ export const actionCreator = () => {
|
||||
})
|
||||
.catch((err) => {
|
||||
const reason = err.response?.data.reason
|
||||
|
||||
enqueueSnackbar(`Failed to switch history. Reason: ${reason}`, {
|
||||
variant: "error",
|
||||
})
|
||||
@@ -669,6 +692,7 @@ export const actionCreator = () => {
|
||||
skipPreferentialTrial,
|
||||
removePreferentialHistory,
|
||||
restorePreferentialHistory,
|
||||
updateFeedbackComponent,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ interface StudyDetailResponse {
|
||||
preferences?: [number, number][]
|
||||
preference_history?: PreferenceHistoryResponce[]
|
||||
plotly_graph_objects: PlotlyGraphObject[]
|
||||
feedback_component_type: FeedbackComponentType
|
||||
skipped_trial_numbers?: number[]
|
||||
}
|
||||
|
||||
@@ -135,6 +136,7 @@ export const getStudyDetailAPI = (
|
||||
objective_names: res.data.objective_names,
|
||||
form_widgets: res.data.form_widgets,
|
||||
is_preferential: res.data.is_preferential,
|
||||
feedback_component_type: res.data.feedback_component_type,
|
||||
preferences: res.data.preferences,
|
||||
preference_history: res.data.preference_history?.map(
|
||||
convertPreferenceHistory
|
||||
@@ -395,3 +397,17 @@ export const restorePreferentialHistoryAPI = (
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
export const reportFeedbackComponentAPI = (
|
||||
studyId: number,
|
||||
component_type: FeedbackComponentType
|
||||
): Promise<void> => {
|
||||
return axiosInstance
|
||||
.put<void>(
|
||||
`/api/studies/${studyId}/preference_feedback_component`,
|
||||
component_type
|
||||
)
|
||||
.then(() => {
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
@@ -32,22 +32,23 @@ export const BestTrialsCard: FC<{
|
||||
header = `Best Trial (number=${bestTrial.number})`
|
||||
content = (
|
||||
<>
|
||||
{bestTrial.values === undefined || bestTrial.values.length === 1 ? (
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
marginBottom: theme.spacing(2),
|
||||
}}
|
||||
color="secondary"
|
||||
>
|
||||
{bestTrial.values}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography>
|
||||
Objective Values = [{bestTrial.values?.join(", ")}]
|
||||
</Typography>
|
||||
)}
|
||||
{!studyDetail?.is_preferential &&
|
||||
(bestTrial.values === undefined || bestTrial.values.length === 1 ? (
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
marginBottom: theme.spacing(2),
|
||||
}}
|
||||
color="secondary"
|
||||
>
|
||||
{bestTrial.values}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography>
|
||||
Objective Values = [{bestTrial.values?.join(", ")}]
|
||||
</Typography>
|
||||
))}
|
||||
<Typography>
|
||||
Params = [
|
||||
{bestTrial.params
|
||||
|
||||
@@ -16,9 +16,11 @@ import Modal from "@mui/material/Modal"
|
||||
import { red } from "@mui/material/colors"
|
||||
|
||||
import { TrialListDetail } from "./TrialList"
|
||||
import { MarkdownRenderer } from "./Note"
|
||||
import { getArtifactUrlPath } from "./PreferentialTrials"
|
||||
import { formatDate } from "../dateUtil"
|
||||
import { actionCreator } from "../action"
|
||||
import { useStudyDetailValue } from "../state"
|
||||
import { PreferentialOutputComponent } from "./PreferentialOutputComponent"
|
||||
|
||||
type TrialType = "worst" | "none"
|
||||
|
||||
@@ -29,8 +31,24 @@ const CandidateTrial: FC<{
|
||||
const theme = useTheme()
|
||||
const trialWidth = 300
|
||||
const trialHeight = 300
|
||||
const studyDetail = useStudyDetailValue(trial.study_id)
|
||||
const [detailShown, setDetailShown] = useState(false)
|
||||
|
||||
if (studyDetail === null) {
|
||||
return null
|
||||
}
|
||||
const componentType = studyDetail.feedback_component_type
|
||||
const artifactId =
|
||||
componentType.output_type === "artifact"
|
||||
? trial.user_attrs.find((a) => a.key === componentType.artifact_key)
|
||||
?.value
|
||||
: undefined
|
||||
const artifact = trial.artifacts.find((a) => a.artifact_id === artifactId)
|
||||
const urlPath =
|
||||
artifactId !== undefined
|
||||
? getArtifactUrlPath(trial.study_id, trial.trial_id, artifactId)
|
||||
: ""
|
||||
|
||||
const cardComponentSx = {
|
||||
padding: 0,
|
||||
position: "relative",
|
||||
@@ -79,7 +97,12 @@ const CandidateTrial: FC<{
|
||||
padding: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<MarkdownRenderer body={trial.note.body} />
|
||||
<PreferentialOutputComponent
|
||||
trial={trial}
|
||||
artifact={artifact}
|
||||
componentType={componentType}
|
||||
urlPath={urlPath}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{type === "worst" ? (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useState, useCallback, useMemo, useEffect } from "react"
|
||||
import React, { FC, useState, useCallback, useEffect } from "react"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Box,
|
||||
Chip,
|
||||
} from "@mui/material"
|
||||
import { MarkdownRenderer } from "./Note"
|
||||
import ReactFlow, {
|
||||
Node,
|
||||
NodeProps,
|
||||
@@ -24,6 +23,10 @@ import "reactflow/dist/style.css"
|
||||
import ELK from "elkjs/lib/elk.bundled.js"
|
||||
import { ElkNode } from "elkjs/lib/elk-api.js"
|
||||
|
||||
import { useStudyDetailValue } from "../state"
|
||||
import { getArtifactUrlPath } from "./PreferentialTrials"
|
||||
import { PreferentialOutputComponent } from "./PreferentialOutputComponent"
|
||||
|
||||
const elk = new ELK()
|
||||
const nodeWidth = 400
|
||||
const nodeHeight = 300
|
||||
@@ -39,10 +42,22 @@ const GraphNode: FC<NodeProps<NodeData>> = ({ data, isConnectable }) => {
|
||||
if (trial === undefined) {
|
||||
return null
|
||||
}
|
||||
const noteBody = trial.note.body
|
||||
const noteFC = useMemo(() => {
|
||||
return <MarkdownRenderer body={noteBody} />
|
||||
}, [noteBody])
|
||||
const studyDetail = useStudyDetailValue(trial.study_id)
|
||||
const componentType = studyDetail?.feedback_component_type
|
||||
if (componentType === undefined) {
|
||||
return null
|
||||
}
|
||||
const artifactId =
|
||||
componentType.output_type === "artifact"
|
||||
? trial.user_attrs.find((a) => a.key === componentType.artifact_key)
|
||||
?.value
|
||||
: undefined
|
||||
const artifact = trial.artifacts.find((a) => a.artifact_id === artifactId)
|
||||
const urlPath =
|
||||
artifactId !== undefined
|
||||
? getArtifactUrlPath(trial.study_id, trial.trial_id, artifactId)
|
||||
: ""
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
@@ -76,7 +91,14 @@ const GraphNode: FC<NodeProps<NodeData>> = ({ data, isConnectable }) => {
|
||||
style={{ background: "#555" }}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
<CardContent>{noteFC}</CardContent>
|
||||
<CardContent>
|
||||
<PreferentialOutputComponent
|
||||
trial={trial}
|
||||
artifact={artifact}
|
||||
componentType={componentType}
|
||||
urlPath={urlPath}
|
||||
/>
|
||||
</CardContent>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { FC, useMemo } from "react"
|
||||
import { ArtifactCardMedia } from "./ArtifactCardMedia"
|
||||
import { MarkdownRenderer } from "./Note"
|
||||
|
||||
export const PreferentialOutputComponent: FC<{
|
||||
trial: Trial
|
||||
artifact?: Artifact
|
||||
componentType: FeedbackComponentType
|
||||
urlPath: string
|
||||
}> = ({ trial, artifact, componentType, urlPath }) => {
|
||||
const note = useMemo(() => {
|
||||
return <MarkdownRenderer body={trial.note.body} />
|
||||
}, [trial.note.body])
|
||||
if (componentType === undefined || componentType.output_type === "note") {
|
||||
return note
|
||||
}
|
||||
if (componentType.output_type === "artifact") {
|
||||
if (artifact === undefined) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<ArtifactCardMedia artifact={artifact} urlPath={urlPath} height="100%" />
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useState } from "react"
|
||||
import React, { FC, useEffect, useState } from "react"
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
@@ -6,33 +6,214 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardActions,
|
||||
CardActionArea,
|
||||
Button,
|
||||
MenuItem,
|
||||
Select,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Modal,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from "@mui/material"
|
||||
import ClearIcon from "@mui/icons-material/Clear"
|
||||
import IconButton from "@mui/material/IconButton"
|
||||
import OpenInFullIcon from "@mui/icons-material/OpenInFull"
|
||||
import ReplayIcon from "@mui/icons-material/Replay"
|
||||
import Modal from "@mui/material/Modal"
|
||||
import { red } from "@mui/material/colors"
|
||||
import UndoIcon from "@mui/icons-material/Undo"
|
||||
import ClearIcon from "@mui/icons-material/Clear"
|
||||
import SettingsIcon from "@mui/icons-material/Settings"
|
||||
import FullscreenIcon from "@mui/icons-material/Fullscreen"
|
||||
|
||||
import { actionCreator } from "../action"
|
||||
import { TrialListDetail } from "./TrialList"
|
||||
import { MarkdownRenderer } from "./Note"
|
||||
import {
|
||||
isThreejsArtifact,
|
||||
useThreejsArtifactModal,
|
||||
} from "./ThreejsArtifactViewer"
|
||||
import { PreferentialOutputComponent } from "./PreferentialOutputComponent"
|
||||
|
||||
const SettingsPage: FC<{
|
||||
studyDetail: StudyDetail
|
||||
settingShown: boolean
|
||||
setSettingShown: (flag: boolean) => void
|
||||
}> = ({ studyDetail, settingShown, setSettingShown }) => {
|
||||
const actions = actionCreator()
|
||||
const [outputComponentType, setOutputComponentType] = useState(
|
||||
studyDetail.feedback_component_type.output_type
|
||||
)
|
||||
const [artifactKey, setArtifactKey] = useState(
|
||||
studyDetail.feedback_component_type.output_type === "artifact"
|
||||
? studyDetail.feedback_component_type.artifact_key
|
||||
: undefined
|
||||
)
|
||||
useEffect(() => {
|
||||
setOutputComponentType(studyDetail.feedback_component_type.output_type)
|
||||
}, [studyDetail.feedback_component_type.output_type])
|
||||
useEffect(() => {
|
||||
if (studyDetail.feedback_component_type.output_type === "artifact") {
|
||||
setArtifactKey(studyDetail.feedback_component_type.artifact_key)
|
||||
}
|
||||
}, [
|
||||
studyDetail.feedback_component_type.output_type === "artifact"
|
||||
? studyDetail.feedback_component_type.artifact_key
|
||||
: undefined,
|
||||
])
|
||||
const onClose = () => {
|
||||
setSettingShown(false)
|
||||
}
|
||||
const onApply = () => {
|
||||
setSettingShown(false)
|
||||
const outputComponent: FeedbackComponentType =
|
||||
outputComponentType === "note"
|
||||
? ({ output_type: "note" } as FeedbackComponentNote)
|
||||
: ({
|
||||
output_type: "artifact",
|
||||
artifact_key: artifactKey,
|
||||
} as FeedbackComponentArtifact)
|
||||
actions.updateFeedbackComponent(studyDetail.id, outputComponent)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={settingShown}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth={true}
|
||||
>
|
||||
<DialogTitle>Settings</DialogTitle>
|
||||
<DialogContent
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Output Component:</FormLabel>
|
||||
<Select
|
||||
value={outputComponentType}
|
||||
onChange={(e) => {
|
||||
setOutputComponentType(e.target.value as "note" | "artifact")
|
||||
}}
|
||||
>
|
||||
<MenuItem value="note">Note</MenuItem>
|
||||
<MenuItem value="artifact">Artifact</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
{outputComponentType === "artifact" ? (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
disabled={studyDetail.union_user_attrs.length === 0}
|
||||
>
|
||||
<FormLabel component="legend">
|
||||
User Attribute Key Corresponding to Output Artifact Id:
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={
|
||||
studyDetail.union_user_attrs.length !== 0
|
||||
? artifactKey ?? ""
|
||||
: "error"
|
||||
}
|
||||
onChange={(e) => {
|
||||
setArtifactKey(e.target.value)
|
||||
}}
|
||||
>
|
||||
{studyDetail.union_user_attrs.length === 0 ? (
|
||||
<MenuItem value="error">No user attributes</MenuItem>
|
||||
) : null}
|
||||
{studyDetail.union_user_attrs.map((attr, index) => {
|
||||
return (
|
||||
<MenuItem key={index} value={attr.key}>
|
||||
{attr.key}
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onApply}
|
||||
color="primary"
|
||||
disabled={
|
||||
outputComponentType === "artifact" && artifactKey === undefined
|
||||
}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const isComparisonReady = (
|
||||
trial: Trial,
|
||||
componentType: FeedbackComponentType
|
||||
): boolean => {
|
||||
if (componentType === undefined || componentType.output_type === "note") {
|
||||
return trial.note.body !== ""
|
||||
}
|
||||
if (componentType.output_type === "artifact") {
|
||||
const artifactId = trial?.user_attrs.find(
|
||||
(a) => a.key === componentType.artifact_key
|
||||
)?.value
|
||||
const artifact = trial?.artifacts.find((a) => a.artifact_id === artifactId)
|
||||
return artifact !== undefined
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const getArtifactUrlPath = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
artifactId: string
|
||||
): string => {
|
||||
return `/artifacts/${studyId}/${trialId}/${artifactId}`
|
||||
}
|
||||
|
||||
const PreferentialTrial: FC<{
|
||||
trial?: Trial
|
||||
studyDetail: StudyDetail
|
||||
candidates: number[]
|
||||
hideTrial: () => void
|
||||
}> = ({ trial, candidates, hideTrial }) => {
|
||||
openDetailTrial: () => void
|
||||
openThreejsArtifactModal: (urlPath: string, artifact: Artifact) => void
|
||||
}> = ({
|
||||
trial,
|
||||
studyDetail,
|
||||
candidates,
|
||||
hideTrial,
|
||||
openDetailTrial,
|
||||
openThreejsArtifactModal,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const action = actionCreator()
|
||||
const trialWidth = 500
|
||||
const [buttonHover, setButtonHover] = useState(false)
|
||||
const trialWidth = 400
|
||||
const trialHeight = 300
|
||||
const [detailShown, setDetailShown] = useState(false)
|
||||
const componentType = studyDetail.feedback_component_type
|
||||
const artifactId =
|
||||
componentType.output_type === "artifact"
|
||||
? trial?.user_attrs.find((a) => a.key === componentType.artifact_key)
|
||||
?.value
|
||||
: undefined
|
||||
const artifact = trial?.artifacts.find((a) => a.artifact_id === artifactId)
|
||||
const urlPath =
|
||||
trial !== undefined && artifactId !== undefined
|
||||
? getArtifactUrlPath(studyDetail.id, trial?.trial_id, artifactId)
|
||||
: ""
|
||||
const is3dModel =
|
||||
componentType.output_type === "artifact" &&
|
||||
artifact !== undefined &&
|
||||
isThreejsArtifact(artifact)
|
||||
|
||||
if (trial == undefined) {
|
||||
if (trial === undefined) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -44,7 +225,11 @@ const PreferentialTrial: FC<{
|
||||
)
|
||||
}
|
||||
|
||||
const isBestTrial = trial.state === "Complete"
|
||||
const onFeedback = () => {
|
||||
hideTrial()
|
||||
action.updatePreference(trial.study_id, candidates, trial.number)
|
||||
}
|
||||
const isReady = isComparisonReady(trial, componentType)
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -53,10 +238,47 @@ const PreferentialTrial: FC<{
|
||||
minHeight: trialHeight,
|
||||
margin: theme.spacing(2),
|
||||
padding: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<CardActions>
|
||||
<Typography variant="h5">Trial {trial.number}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
margin: theme.spacing(0, 2),
|
||||
maxWidth: `calc(${trialWidth}px - ${
|
||||
is3dModel ? theme.spacing(8) : theme.spacing(4)
|
||||
})`,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">Trial {trial.number}</Typography>
|
||||
{componentType.output_type === "artifact" &&
|
||||
artifact !== undefined ? (
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
margin: theme.spacing(0, 2),
|
||||
}}
|
||||
>
|
||||
{`(${artifact.filename})`}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
{is3dModel ? (
|
||||
<IconButton
|
||||
aria-label="show artifact 3d model"
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ marginLeft: "auto" }}
|
||||
onClick={() => {
|
||||
openThreejsArtifactModal(urlPath, artifact)
|
||||
}}
|
||||
>
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
<IconButton
|
||||
sx={{
|
||||
marginLeft: "auto",
|
||||
@@ -73,107 +295,98 @@ const PreferentialTrial: FC<{
|
||||
sx={{
|
||||
marginLeft: "auto",
|
||||
}}
|
||||
onClick={() => setDetailShown(true)}
|
||||
onClick={openDetailTrial}
|
||||
aria-label="show detail"
|
||||
>
|
||||
<OpenInFullIcon />
|
||||
</IconButton>
|
||||
</CardActions>
|
||||
<CardActionArea>
|
||||
<CardContent
|
||||
aria-label="trial-button"
|
||||
onClick={() => {
|
||||
hideTrial()
|
||||
action.updatePreference(trial.study_id, candidates, trial.number)
|
||||
}}
|
||||
sx={{
|
||||
padding: 0,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
"::before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor:
|
||||
theme.palette.mode === "dark" ? "white" : "black",
|
||||
opacity: 0,
|
||||
zIndex: 1,
|
||||
transition: "opacity 0.3s ease-out",
|
||||
},
|
||||
":hover::before": {
|
||||
opacity: 0.2,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
padding: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
{trial.note.body !== "" ? (
|
||||
<MarkdownRenderer body={trial.note.body} />
|
||||
) : (
|
||||
<CircularProgress />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<ClearIcon
|
||||
sx={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
top: 0,
|
||||
left: 0,
|
||||
color: red[600],
|
||||
opacity: 0,
|
||||
transition: "opacity 0.3s ease-out",
|
||||
zIndex: 1,
|
||||
":hover": {
|
||||
opacity: 0.3,
|
||||
filter:
|
||||
theme.palette.mode === "dark"
|
||||
<CardContent
|
||||
aria-label="trial-button"
|
||||
onClick={(e) => {
|
||||
if (e.shiftKey) onFeedback()
|
||||
}}
|
||||
sx={{
|
||||
position: "relative",
|
||||
padding: theme.spacing(2),
|
||||
overflow: "hidden",
|
||||
minHeight: theme.spacing(20),
|
||||
}}
|
||||
>
|
||||
{isReady ? (
|
||||
<>
|
||||
<PreferentialOutputComponent
|
||||
trial={trial}
|
||||
artifact={artifact}
|
||||
componentType={componentType}
|
||||
urlPath={urlPath}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor:
|
||||
theme.palette.mode === "dark" ? "white" : "black",
|
||||
opacity: buttonHover ? 0.2 : 0,
|
||||
zIndex: 1,
|
||||
transition: "opacity 0.3s ease-out",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
<ClearIcon
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
color: red[600],
|
||||
opacity: buttonHover ? 0.3 : 0,
|
||||
transition: "opacity 0.3s ease-out",
|
||||
zIndex: 1,
|
||||
filter: buttonHover
|
||||
? theme.palette.mode === "dark"
|
||||
? "brightness(1.1)"
|
||||
: "brightness(1.7)",
|
||||
},
|
||||
: "brightness(1.7)"
|
||||
: "none",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<CircularProgress
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
margin: "auto",
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
<Modal open={detailShown} onClose={() => setDetailShown(false)}>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: "80%",
|
||||
maxHeight: "90%",
|
||||
margin: "auto",
|
||||
overflow: "hidden",
|
||||
backgroundColor: theme.palette.mode === "dark" ? "black" : "white",
|
||||
borderRadius: theme.spacing(3),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<TrialListDetail
|
||||
trial={trial}
|
||||
isBestTrial={() => isBestTrial}
|
||||
directions={[]}
|
||||
objectiveNames={[]}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
)}
|
||||
</CardContent>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onFeedback}
|
||||
onMouseEnter={() => {
|
||||
setButtonHover(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setButtonHover(false)
|
||||
}}
|
||||
color="error"
|
||||
disabled={!isReady && candidates.length > 0}
|
||||
sx={{
|
||||
marginTop: "auto",
|
||||
}}
|
||||
>
|
||||
<ClearIcon />
|
||||
Worst
|
||||
</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -186,13 +399,17 @@ type DisplayTrials = {
|
||||
export const PreferentialTrials: FC<{ studyDetail: StudyDetail | null }> = ({
|
||||
studyDetail,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const action = actionCreator()
|
||||
const [undoHistoryId, setUndoHistoryId] = useState<string | null>(null)
|
||||
const [openThreejsArtifactModal, renderThreejsArtifactModal] =
|
||||
useThreejsArtifactModal()
|
||||
const [displayTrials, setDisplayTrials] = useState<DisplayTrials>({
|
||||
display: [],
|
||||
clicked: [],
|
||||
})
|
||||
const theme = useTheme()
|
||||
const action = actionCreator()
|
||||
const [settingShown, setSettingShown] = useState(false)
|
||||
const [detailTrial, setDetailTrial] = useState<number | null>(null)
|
||||
|
||||
if (studyDetail === null || !studyDetail.is_preferential) {
|
||||
return null
|
||||
@@ -274,34 +491,124 @@ export const PreferentialTrials: FC<{ studyDetail: StudyDetail | null }> = ({
|
||||
>
|
||||
Which trial is the worst?
|
||||
</Typography>
|
||||
<IconButton
|
||||
disabled={latestHistoryId === null || undoHistoryId !== null}
|
||||
onClick={() => {
|
||||
if (latestHistoryId === null) {
|
||||
return
|
||||
}
|
||||
setUndoHistoryId(latestHistoryId)
|
||||
action.removePreferentialHistory(studyDetail.id, latestHistoryId)
|
||||
}}
|
||||
<Box
|
||||
display="flex"
|
||||
sx={{
|
||||
margin: "auto 0 auto auto",
|
||||
marginLeft: "auto",
|
||||
marginY: "auto",
|
||||
}}
|
||||
>
|
||||
<UndoIcon />
|
||||
</IconButton>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={latestHistoryId === null || undoHistoryId !== null}
|
||||
onClick={() => {
|
||||
if (latestHistoryId === null) {
|
||||
return
|
||||
}
|
||||
setUndoHistoryId(latestHistoryId)
|
||||
action.removePreferentialHistory(studyDetail.id, latestHistoryId)
|
||||
}}
|
||||
sx={{
|
||||
marginY: "auto",
|
||||
marginRight: theme.spacing(2),
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<UndoIcon />
|
||||
Undo
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{
|
||||
marginY: "auto",
|
||||
marginRight: theme.spacing(2),
|
||||
justifySelf: "end",
|
||||
}}
|
||||
onClick={() => setSettingShown(true)}
|
||||
>
|
||||
<SettingsIcon />
|
||||
Settings
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", flexDirection: "row", flexWrap: "wrap" }}>
|
||||
{displayTrials.display.map((t, index) => (
|
||||
<PreferentialTrial
|
||||
key={t == -1 ? -index - 1 : t}
|
||||
trial={activeTrials.find((trial) => trial.number === t)}
|
||||
candidates={displayTrials.display.filter((n) => n !== -1)}
|
||||
hideTrial={() => {
|
||||
hideTrial(t)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{displayTrials.display.map((t, index) => {
|
||||
const trial = activeTrials.find((trial) => trial.number === t)
|
||||
const candidates = displayTrials.display.filter(
|
||||
(n) =>
|
||||
n !== -1 &&
|
||||
isComparisonReady(
|
||||
studyDetail.trials[n],
|
||||
studyDetail.feedback_component_type
|
||||
)
|
||||
)
|
||||
return (
|
||||
<PreferentialTrial
|
||||
key={t === -1 ? -index - 1 : t}
|
||||
trial={trial}
|
||||
studyDetail={studyDetail}
|
||||
candidates={candidates}
|
||||
hideTrial={() => hideTrial(t)}
|
||||
openDetailTrial={() => setDetailTrial(t)}
|
||||
openThreejsArtifactModal={openThreejsArtifactModal}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
<SettingsPage
|
||||
settingShown={settingShown}
|
||||
setSettingShown={setSettingShown}
|
||||
studyDetail={studyDetail}
|
||||
/>
|
||||
{detailTrial !== null && (
|
||||
<Modal open={true} onClose={() => setDetailTrial(null)}>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: "80%",
|
||||
maxHeight: "90%",
|
||||
margin: "auto",
|
||||
overflow: "hidden",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
borderRadius: theme.spacing(3),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "auto",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: theme.spacing(2),
|
||||
right: theme.spacing(2),
|
||||
}}
|
||||
onClick={() => setDetailTrial(null)}
|
||||
>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
<TrialListDetail
|
||||
trial={studyDetail.trials[detailTrial]}
|
||||
isBestTrial={(trialId) =>
|
||||
studyDetail.trials.find((t) => t.trial_id === trialId)
|
||||
?.state === "Complete" ?? false
|
||||
}
|
||||
directions={[]}
|
||||
objectiveNames={[]}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
)}
|
||||
{renderThreejsArtifactModal()}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Vendored
+12
@@ -187,6 +187,17 @@ type PlotlyGraphObject = {
|
||||
graph_object: string
|
||||
}
|
||||
|
||||
type FeedbackComponentNote = {
|
||||
output_type: "note"
|
||||
}
|
||||
|
||||
type FeedbackComponentArtifact = {
|
||||
output_type: "artifact"
|
||||
artifact_key: string
|
||||
}
|
||||
|
||||
type FeedbackComponentType = FeedbackComponentArtifact | FeedbackComponentNote
|
||||
|
||||
type StudyDetail = {
|
||||
id: number
|
||||
name: string
|
||||
@@ -203,6 +214,7 @@ type StudyDetail = {
|
||||
is_preferential: boolean
|
||||
objective_names?: string[]
|
||||
form_widgets?: FormWidgets
|
||||
feedback_component_type: FeedbackComponentType
|
||||
preferences?: [number, number][]
|
||||
preference_history?: PreferenceHistory[]
|
||||
plotly_graph_objects: PlotlyGraphObject[]
|
||||
|
||||
@@ -8,6 +8,7 @@ from optuna import get_all_study_summaries
|
||||
from optuna.study import StudyDirection
|
||||
from optuna_dashboard._app import create_app
|
||||
from optuna_dashboard._app import create_new_study
|
||||
from optuna_dashboard._preference_setting import register_preference_feedback_component
|
||||
from optuna_dashboard._preferential_history import NewHistory
|
||||
from optuna_dashboard._preferential_history import remove_history
|
||||
from optuna_dashboard._preferential_history import report_history
|
||||
@@ -183,6 +184,36 @@ class APITestCase(TestCase):
|
||||
)
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
def test_change_component(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = create_study(storage=storage, n_generate=3)
|
||||
register_preference_feedback_component(study, "note")
|
||||
for _ in range(3):
|
||||
study.ask()
|
||||
|
||||
app = create_app(storage)
|
||||
study_id = study._study._study_id
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/studies/{study_id}/preference_feedback_component",
|
||||
"PUT",
|
||||
body=json.dumps({"output_type": "artifact", "artifact_key": "image"}),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 204)
|
||||
|
||||
status, _, body = send_request(
|
||||
app,
|
||||
f"/api/studies/{study_id}",
|
||||
"GET",
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 200)
|
||||
|
||||
study_detail = json.loads(body)
|
||||
assert study_detail["feedback_component_type"]["output_type"] == "artifact"
|
||||
assert study_detail["feedback_component_type"]["artifact_key"] == "image"
|
||||
|
||||
def test_skip_trial(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = create_study(n_generate=4, storage=storage)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
import optuna
|
||||
from optuna_dashboard._preference_setting import _SYSTEM_ATTR_FEEDBACK_COMPONENT
|
||||
from optuna_dashboard._preference_setting import register_preference_feedback_component
|
||||
from optuna_dashboard.preferential._study import PreferentialStudy
|
||||
|
||||
|
||||
class FeedbackSettingTestCase(TestCase):
|
||||
def test_widget_to_dict_from_dict(self) -> None:
|
||||
study = PreferentialStudy(optuna.create_study())
|
||||
register_preference_feedback_component(study, "artifact", "image_key")
|
||||
system_attrs = study._study.system_attrs
|
||||
feedback_type = system_attrs.get(_SYSTEM_ATTR_FEEDBACK_COMPONENT, {})
|
||||
assert "output_type" in feedback_type
|
||||
assert feedback_type["output_type"] == "artifact"
|
||||
assert "artifact_key" in feedback_type
|
||||
assert feedback_type["artifact_key"] == "image_key"
|
||||
Reference in New Issue
Block a user