mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-09 11:28:14 +08:00
Merge branch 'main' into artifact
This commit is contained in:
@@ -25,6 +25,7 @@ from optuna.storages import RDBStorage
|
||||
from optuna.study import StudyDirection
|
||||
from optuna.study import StudySummary
|
||||
from optuna.trial import FrozenTrial
|
||||
from optuna.trial import TrialState
|
||||
from optuna.version import __version__ as optuna_ver
|
||||
from packaging import version
|
||||
|
||||
@@ -405,6 +406,45 @@ def create_app(
|
||||
response.status = 204 # No content
|
||||
return {}
|
||||
|
||||
@app.post("/api/trials/<trial_id:int>/tell")
|
||||
@json_api_view
|
||||
def tell_trial(trial_id: int) -> BottleViewReturn:
|
||||
|
||||
if "state" not in request.json:
|
||||
response.status = 400 # Bad request
|
||||
return {"reason": "state must be specified."}
|
||||
|
||||
try:
|
||||
state = TrialState[request.json["state"].upper()]
|
||||
except Exception: # To catch KeyError and Exception by non str case.
|
||||
response.status = 400 # Bad request
|
||||
return {"reason": "state must be either 'Complete' or 'Fail'."}
|
||||
|
||||
if state not in [TrialState.COMPLETE, TrialState.FAIL]:
|
||||
response.status = 400 # Bad request
|
||||
return {"reason": "state must be either 'Complete' or 'Fail'."}
|
||||
|
||||
values = None
|
||||
if state == TrialState.COMPLETE:
|
||||
vs = request.json.get("values")
|
||||
if vs is None:
|
||||
response.status = 400 # Bad request
|
||||
return {"reason": "values attribute is required when state is 'Complete'."}
|
||||
try:
|
||||
values = [float(v) for v in vs]
|
||||
except (ValueError, TypeError):
|
||||
response.status = 400 # Bad request
|
||||
return {"reason": "values attribute must be an array of numbers"}
|
||||
|
||||
try:
|
||||
storage.set_trial_state_values(trial_id, state, values)
|
||||
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]:
|
||||
|
||||
@@ -86,7 +86,7 @@ def get_param_importance_from_trials_cache(
|
||||
) -> list[ImportanceType]:
|
||||
completed_trials = [t for t in trials if t.state == TrialState.COMPLETE]
|
||||
n_completed_trials = len(completed_trials)
|
||||
if n_completed_trials == 0:
|
||||
if n_completed_trials <= 1:
|
||||
return []
|
||||
|
||||
cache_key = f"{study_id}:{objective_id}"
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
deleteStudyAPI,
|
||||
saveStudyNoteAPI,
|
||||
saveTrialNoteAPI,
|
||||
tellTrialAPI,
|
||||
renameStudyAPI,
|
||||
uploadArtifactAPI,
|
||||
getMetaInfoAPI,
|
||||
@@ -95,6 +96,25 @@ export const actionCreator = () => {
|
||||
setTrialArtifacts(studyId, index, newArtifacts)
|
||||
}
|
||||
|
||||
const setTrialStateValues = (
|
||||
studyId: number,
|
||||
index: number,
|
||||
state: TrialState,
|
||||
values?: TrialValueNumber[]
|
||||
) => {
|
||||
const newTrial: Trial = Object.assign(
|
||||
{},
|
||||
studyDetails[studyId].trials[index]
|
||||
)
|
||||
newTrial.state = state
|
||||
newTrial.values = values
|
||||
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[][]
|
||||
@@ -369,6 +389,44 @@ export const actionCreator = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const tellTrial = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
state: TrialStateFinished,
|
||||
values?: number[]
|
||||
): Promise<void> => {
|
||||
const message =
|
||||
values === undefined
|
||||
? `id=${trialId}, state=${state}`
|
||||
: `id=${trialId}, state=${state}, values=${values}`
|
||||
return tellTrialAPI(trialId, state, values)
|
||||
.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
|
||||
}
|
||||
setTrialStateValues(studyId, index, state, values)
|
||||
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,
|
||||
@@ -383,6 +441,7 @@ export const actionCreator = () => {
|
||||
saveTrialNote,
|
||||
uploadArtifact,
|
||||
deleteArtifact,
|
||||
tellTrial,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -262,6 +262,23 @@ export const deleteArtifactAPI = (
|
||||
})
|
||||
}
|
||||
|
||||
export const tellTrialAPI = (
|
||||
trialId: number,
|
||||
state: TrialStateFinished,
|
||||
values?: number[]
|
||||
): Promise<void> => {
|
||||
const req: { state: TrialState; values?: number[] } = {
|
||||
state: state,
|
||||
values: values,
|
||||
}
|
||||
|
||||
return axiosInstance
|
||||
.post<void>(`/api/trials/${trialId}/tell`, req)
|
||||
.then((res) => {
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
interface ParamImportancesResponse {
|
||||
param_importances: ParamImportance[][]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import React, { FC } from "react"
|
||||
import { Typography, Grid, Box, IconButton } from "@mui/material"
|
||||
import React, { createRef, FC, FormEvent, MouseEvent } from "react"
|
||||
import {
|
||||
Typography,
|
||||
Grid,
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Stack,
|
||||
TextField,
|
||||
} from "@mui/material"
|
||||
import LinkIcon from "@mui/icons-material/Link"
|
||||
|
||||
import { DataGridColumn, DataGrid } from "./DataGrid"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
import { actionCreator } from "../action"
|
||||
|
||||
export const TrialTable: FC<{
|
||||
studyDetail: StudyDetail | null
|
||||
isBeta: boolean
|
||||
@@ -12,6 +22,7 @@ export const TrialTable: FC<{
|
||||
}> = ({ studyDetail, isBeta, initialRowsPerPage }) => {
|
||||
const trials: Trial[] = studyDetail !== null ? studyDetail.trials : []
|
||||
const objectiveNames: string[] = studyDetail?.objective_names || []
|
||||
const action = actionCreator()
|
||||
|
||||
const columns: DataGridColumn<Trial>[] = [
|
||||
{ field: "number", label: "Number", sortable: true, padding: "none" },
|
||||
@@ -263,6 +274,39 @@ export const TrialTable: FC<{
|
||||
]
|
||||
|
||||
const collapseBody = (index: number) => {
|
||||
const objectiveFormRefs = studyDetail?.directions.map((d) =>
|
||||
createRef<HTMLInputElement>()
|
||||
)
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
|
||||
if (objectiveFormRefs === undefined) {
|
||||
return
|
||||
}
|
||||
if (studyDetail === null) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
const studyId = studyDetail.id
|
||||
const trialId = trials[index].trial_id
|
||||
const objectiveValues = objectiveFormRefs.map((ref) =>
|
||||
ref.current ? Number(ref.current.value) : NaN
|
||||
)
|
||||
if (objectiveValues.includes(NaN)) {
|
||||
return
|
||||
}
|
||||
|
||||
action.tellTrial(studyId, trialId, "Complete", objectiveValues)
|
||||
}
|
||||
|
||||
const handleFailTrial = (e: MouseEvent<HTMLButtonElement>): void => {
|
||||
if (studyDetail === null) {
|
||||
return
|
||||
}
|
||||
const studyId = studyDetail.id
|
||||
const trialId = trials[index].trial_id
|
||||
action.tellTrial(studyId, trialId, "Fail")
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={6}>
|
||||
@@ -293,6 +337,55 @@ export const TrialTable: FC<{
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
{trials[index].state === "Running" ? (
|
||||
<Grid item xs={12}>
|
||||
<Box margin={1}>
|
||||
<Typography variant="h6" gutterBottom component="div">
|
||||
Trial tell
|
||||
</Typography>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Box margin={1}>
|
||||
<Stack direction="row" spacing={1}>
|
||||
{objectiveFormRefs !== undefined &&
|
||||
objectiveFormRefs.map((ref, i) => (
|
||||
<TextField
|
||||
required
|
||||
id={`objective-${i}`}
|
||||
key={`objective-${i}`}
|
||||
label={
|
||||
objectiveNames.length ===
|
||||
studyDetail?.directions.length
|
||||
? objectiveNames[i]
|
||||
: `Objective ${i}`
|
||||
}
|
||||
inputProps={{
|
||||
inputMode: "numeric",
|
||||
pattern: "[-+]?[0-9]*.?[0-9]+([eE][-+]?[0-9]+)?",
|
||||
title: "Please input a float number",
|
||||
}}
|
||||
inputRef={ref}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box margin={1}>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Button variant="contained" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={handleFailTrial}
|
||||
>
|
||||
Fail Trial
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
</Grid>
|
||||
) : null}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -10,6 +10,7 @@ declare const URL_PREFIX: string
|
||||
type TrialValueNumber = number | "inf" | "-inf"
|
||||
type TrialIntermediateValueNumber = number | "inf" | "-inf" | "nan"
|
||||
type TrialState = "Running" | "Complete" | "Pruned" | "Fail" | "Waiting"
|
||||
type TrialStateFinished = "Complete" | "Fail" | "Pruned"
|
||||
type StudyDirection = "maximize" | "minimize" | "not_set"
|
||||
|
||||
type FloatDistribution = {
|
||||
|
||||
Generated
+3241
-2017
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -38,7 +38,7 @@
|
||||
"@babel/core": "^7.14.3",
|
||||
"@babel/preset-env": "^7.14.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/jest": "^29.2.1",
|
||||
"@types/plotly.js": "^2.12.11",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
@@ -49,10 +49,11 @@
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"esbuild-loader": "^2.18.0",
|
||||
"eslint": "^7.28.0",
|
||||
"jest": "^27.4.1",
|
||||
"jest": "^29.2.1",
|
||||
"jest-canvas-mock": "^2.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"prettier": "^2.5.1",
|
||||
"ts-jest": "^27.1.3",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-loader": "^9.2.7",
|
||||
"typescript": "^4.6.2",
|
||||
"webpack": "^5.70.0",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from "react"
|
||||
global.URL.createObjectURL = jest.fn()
|
||||
|
||||
import { SnackbarProvider } from "notistack"
|
||||
import { RecoilRoot } from "recoil"
|
||||
import { cleanup, render, within, fireEvent } from "@testing-library/react"
|
||||
import { TrialTable } from "../optuna_dashboard/ts/components/TrialTable"
|
||||
|
||||
@@ -126,7 +128,11 @@ const studyDetail: StudyDetail = {
|
||||
|
||||
it("Sort TrialTable by trial number", () => {
|
||||
const { getAllByRole, getByText } = render(
|
||||
<TrialTable studyDetail={studyDetail} isBeta={false} />
|
||||
<RecoilRoot>
|
||||
<SnackbarProvider>
|
||||
<TrialTable studyDetail={studyDetail} isBeta={false} />
|
||||
</SnackbarProvider>
|
||||
</RecoilRoot>
|
||||
)
|
||||
const rows = getAllByRole("row")
|
||||
|
||||
@@ -142,7 +148,11 @@ it("Sort TrialTable by trial number", () => {
|
||||
|
||||
it("Sort TrialTable by value", () => {
|
||||
const { getAllByRole, getByText } = render(
|
||||
<TrialTable studyDetail={studyDetail} isBeta={false} />
|
||||
<RecoilRoot>
|
||||
<SnackbarProvider>
|
||||
<TrialTable studyDetail={studyDetail} isBeta={false} />
|
||||
</SnackbarProvider>
|
||||
</RecoilRoot>
|
||||
)
|
||||
fireEvent.click(getByText("Value"))
|
||||
const rows = getAllByRole("row")
|
||||
@@ -157,7 +167,11 @@ it("Sort TrialTable by value", () => {
|
||||
|
||||
it("Sort TrialTable by duration", () => {
|
||||
const { getAllByRole, getByText } = render(
|
||||
<TrialTable studyDetail={studyDetail} isBeta={false} />
|
||||
<RecoilRoot>
|
||||
<SnackbarProvider>
|
||||
<TrialTable studyDetail={studyDetail} isBeta={false} />
|
||||
</SnackbarProvider>
|
||||
</RecoilRoot>
|
||||
)
|
||||
fireEvent.click(getByText("Duration(ms)"))
|
||||
const rows = getAllByRole("row")
|
||||
@@ -172,7 +186,11 @@ it("Sort TrialTable by duration", () => {
|
||||
|
||||
it("Sort TrialTable by state", () => {
|
||||
const { getAllByRole, getByText } = render(
|
||||
<TrialTable studyDetail={studyDetail} isBeta={false} />
|
||||
<RecoilRoot>
|
||||
<SnackbarProvider>
|
||||
<TrialTable studyDetail={studyDetail} isBeta={false} />
|
||||
</SnackbarProvider>
|
||||
</RecoilRoot>
|
||||
)
|
||||
fireEvent.click(getByText("State"))
|
||||
const rows = getAllByRole("row")
|
||||
@@ -187,7 +205,11 @@ it("Sort TrialTable by state", () => {
|
||||
|
||||
it("Filter trials by state", () => {
|
||||
const { queryAllByText } = render(
|
||||
<TrialTable studyDetail={studyDetail} isBeta={false} />
|
||||
<RecoilRoot>
|
||||
<SnackbarProvider>
|
||||
<TrialTable studyDetail={studyDetail} isBeta={false} />
|
||||
</SnackbarProvider>
|
||||
</RecoilRoot>
|
||||
)
|
||||
expect(queryAllByText("Fail").length).toBe(1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user