Merge branch 'main' into add_color_scale_setting

This commit is contained in:
gen740
2024-02-07 00:41:41 +09:00
37 changed files with 709 additions and 229 deletions
+1 -1
View File
@@ -48,4 +48,4 @@ jobs:
- name: Run e2e tests
run: |
pytest e2e_tests/test_dashboard
pytest e2e_tests/test_dashboard || true
-2
View File
@@ -1,2 +0,0 @@
optuna_dashboard/ts/components/PlotlyDarkMode.ts
standalone_app/src/PlotlyDarkMode.ts
-4
View File
@@ -1,4 +0,0 @@
trailingComma: "es5"
tabWidth: 2
semi: false
singleQuote: false
+28
View File
@@ -0,0 +1,28 @@
{
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"files": {
"include": [
"optuna_dashboard/ts/**/*.ts",
"optuna_dashboard/ts/**/*.tsx",
"typescript_tests/**/*.ts",
"typescript_tests/**/*.tsx",
"standalone_app/src/**/*.ts",
"standalone_app/src/**/*.tsx",
"vscode/src/**/*.ts",
"vscode/src/**/*.tsx"
],
"ignore": [
"optuna_dashboard/ts/components/PlotlyDarkMode.ts",
"standalone_app/src/PlotlyDarkMode.ts"
]
},
"javascript": {
"formatter": {
"trailingComma": "es5",
"indentWidth": 2,
"indentStyle": "space",
"semicolons": "asNeeded",
"quoteStyle": "double"
}
}
}
+45 -9
View File
@@ -193,14 +193,20 @@ def create_app(
@app.get("/api/studies/<study_id:int>")
@json_api_view
def get_study_detail(study_id: int) -> dict[str, Any]:
try:
after = int(request.params["after"])
assert after >= 0
except AssertionError:
response.status = 400 # Bad parameter
return {"reason": "`after` should be larger or equal 0."}
except KeyError:
after = 0
# Use the following default values if not specified in request.params.
query_params = dict(after=0, limit=2000)
for query_key in query_params:
try:
query_params[query_key] = int(request.params[query_key])
assert query_params[query_key] >= 0
except AssertionError:
response.status = 400 # Bad parameter
return {"reason": f"`{query_key}` should be larger than or equal to 0."}
except KeyError:
# Use the default parameter defined in query_params.
pass
after, limit = query_params["after"], query_params["limit"]
summary = get_study_summary(storage, study_id)
if summary is None:
response.status = 404 # Not found
@@ -231,16 +237,19 @@ def create_app(
plotly_graph_objects = get_plotly_graph_objects(system_attrs)
skipped_trial_ids = get_skipped_trial_ids(system_attrs)
skipped_trial_numbers = [t.number for t in trials if t._trial_id in skipped_trial_ids]
limit = len(trials) if limit == 0 else limit
fetched_trials_partially = after + limit < len(trials)
return serialize_study_detail(
summary,
best_trials,
trials[after:],
trials[after : after + limit],
intersection,
union,
union_user_attrs,
has_intermediate_values,
plotly_graph_objects,
skipped_trial_numbers,
fetched_trials_partially,
)
@app.get("/api/studies/<study_id:int>/param_importances")
@@ -271,6 +280,33 @@ def create_app(
)
if plot_type == "contour":
fig = optuna.visualization.plot_contour(study)
elif plot_type == "slice":
fig = optuna.visualization.plot_slice(study)
# Note: Optuna's implementation forces a minimum width.
# We override it to prevent the figure from going beyond the screen width.
# https://github.com/optuna/optuna/blob/2abd0ae81eaf3683ce1dd580429904c8a705300d/optuna/visualization/_slice.py#L237-L239
fig.update_layout(width=None)
elif plot_type == "parallel_coordinate":
fig = optuna.visualization.plot_parallel_coordinate(study)
elif plot_type == "rank":
fig = optuna.visualization.plot_rank(study)
elif plot_type == "edf":
fig = optuna.visualization.plot_edf(study)
else:
response.status = 404 # Not found
return {"reason": f"plot_type={plot_type} is not supported."}
return fig.to_json()
@app.get("/api/compare-studies/plot/<plot_type>")
@json_api_view
def get_compare_studies_plot(plot_type: str) -> dict[str, Any]:
study_ids = map(int, request.query.getall("study_ids[]"))
studies = [
optuna.load_study(study_name=storage.get_study_name_from_id(study_id), storage=storage)
for study_id in study_ids
]
if plot_type == "edf":
fig = optuna.visualization.plot_edf(studies)
else:
response.status = 404 # Not found
return {"reason": f"plot_type={plot_type} is not supported."}
+2
View File
@@ -141,11 +141,13 @@ def serialize_study_detail(
has_intermediate_values: bool,
plotly_graph_objects: dict[str, str],
skipped_trial_numbers: list[int],
fetched_trials_partially: bool,
) -> dict[str, Any]:
serialized: dict[str, Any] = {
"name": summary.study_name,
"directions": [d.name.lower() for d in summary.directions],
"user_attrs": serialize_attrs(summary.user_attrs),
"fetched_trials_partially": fetched_trials_partially,
}
system_attrs = getattr(summary, "system_attrs", {})
serialized["artifacts"] = list_study_artifacts(system_attrs)
+16 -2
View File
@@ -27,6 +27,7 @@ import {
studySummariesState,
paramImportanceState,
isFileUploading,
fetchedTrialsPartiallyState,
artifactIsAvailable,
plotlypyIsAvailableState,
reloadIntervalState,
@@ -48,6 +49,9 @@ export const actionCreator = () => {
const setUploading = useSetRecoilState<boolean>(isFileUploading)
const setTrialsUpdating = useSetRecoilState(trialsUpdatingState)
const setArtifactIsAvailable = useSetRecoilState<boolean>(artifactIsAvailable)
const setFetchedTrialsPartially = useSetRecoilState<boolean>(
fetchedTrialsPartiallyState
)
const setPlotlypyIsAvailable = useSetRecoilState<boolean>(
plotlypyIsAvailableState
)
@@ -243,8 +247,12 @@ export const actionCreator = () => {
})
}
const updateStudyDetail = (studyId: number) => {
const updateStudyDetail = (
studyId: number,
forceFetchAllTrials: boolean = false
) => {
let nLocalFixedTrials = 0
const nMaximumTrialsAtOnce = forceFetchAllTrials ? 0 : 2000
if (studyId in studyDetails) {
const currentTrials = studyDetails[studyId].trials
const firstUpdatable = currentTrials.findIndex((trial) =>
@@ -253,14 +261,20 @@ export const actionCreator = () => {
nLocalFixedTrials =
firstUpdatable === -1 ? currentTrials.length : firstUpdatable
}
getStudyDetailAPI(studyId, nLocalFixedTrials)
getStudyDetailAPI(studyId, nLocalFixedTrials, nMaximumTrialsAtOnce)
.then((study) => {
if (studyId in studyDetails && study.trials.length === 0) {
// Update trials only if necessary.
// NOTE: The first condition is for study with no trials.
return
}
const currentFixedTrials =
studyId in studyDetails
? studyDetails[studyId].trials.slice(0, nLocalFixedTrials)
: []
study.trials = currentFixedTrials.concat(study.trials)
setStudyDetailState(studyId, study)
setFetchedTrialsPartially(study.fetched_trials_partially)
})
.catch((err) => {
const reason = err.response?.data.reason
+23 -1
View File
@@ -104,16 +104,19 @@ interface StudyDetailResponse {
artifacts: Artifact[]
feedback_component_type: FeedbackComponentType
skipped_trial_numbers?: number[]
fetched_trials_partially: boolean
}
export const getStudyDetailAPI = (
studyId: number,
nLocalTrials: number
nLocalTrials: number,
nMaximumTrialsAtOnce: number
): Promise<StudyDetail> => {
return axiosInstance
.get<StudyDetailResponse>(`/api/studies/${studyId}`, {
params: {
after: nLocalTrials,
limit: nMaximumTrialsAtOnce,
},
})
.then((res) => {
@@ -147,6 +150,7 @@ export const getStudyDetailAPI = (
plotly_graph_objects: res.data.plotly_graph_objects,
artifacts: res.data.artifacts,
skipped_trial_numbers: res.data.skipped_trial_numbers ?? [],
fetched_trials_partially: res.data.fetched_trials_partially,
}
})
}
@@ -448,6 +452,10 @@ type PlotResponse = {
}
export enum PlotType {
Contour = "contour",
Slice = "slice",
ParallelCoordinate = "parallel_coordinate",
Rank = "rank",
EDF = "edf",
}
export const getPlotAPI = (
studyId: number,
@@ -457,3 +465,17 @@ export const getPlotAPI = (
.get<PlotResponse>(`/api/studies/${studyId}/plot/${plotType}`)
.then<PlotResponse>((res) => res.data)
}
export enum CompareStudiesPlotType {
EDF = "edf",
}
export const getCompareStudiesPlotAPI = (
studyIds: number[],
plotType: CompareStudiesPlotType
): Promise<PlotResponse> => {
return axiosInstance
.get<PlotResponse>(`/api/compare-studies/plot/${plotType}`, {
params: { study_ids: studyIds },
})
.then<PlotResponse>((res) => res.data)
}
+6 -1
View File
@@ -324,7 +324,12 @@ export const AppDrawer: FC<{
<ListItemButton
sx={styleListItemButton}
onClick={() => {
action.saveReloadInterval(reloadInterval === -1 ? 10 : -1)
const newReloadInterval = reloadInterval === -1 ? 10 : -1
action.saveReloadInterval(newReloadInterval)
if (newReloadInterval === -1) {
const forceFetchAllTrials = true
action.updateStudyDetail(studyId, forceFetchAllTrials)
}
}}
>
<ListItemIcon sx={styleListItemIcon}>
@@ -11,7 +11,7 @@ import { actionCreator } from "../../action"
export const useDeleteTrialArtifactDialog = (): [
(studyId: number, trialId: number, artifact: Artifact) => void,
() => ReactNode
() => ReactNode,
] => {
const action = actionCreator()
@@ -58,7 +58,7 @@ export const useDeleteTrialArtifactDialog = (): [
export const useDeleteStudyArtifactDialog = (): [
(studyId: number, artifact: Artifact) => void,
() => ReactNode
() => ReactNode,
] => {
const action = actionCreator()
@@ -124,7 +124,7 @@ export const ThreejsArtifactViewer: React.FC<ThreejsArtifactViewerProps> = (
export const useThreejsArtifactModal = (): [
(path: string, artifact: Artifact) => void,
() => ReactNode
() => ReactNode,
] => {
const [open, setOpen] = useState(false)
const [target, setTarget] = useState<[string, Artifact | null]>(["", null])
@@ -34,39 +34,38 @@ const useWavesurfer = (
}
// Create a React component of wavesurfer.
export const WaveSurferArtifactViewer: React.FC<
WaveSurferArtifactViewerProps
> = (props) => {
const containerRef = useRef<HTMLDivElement>(null!)
const [isPlaying, setIsPlaying] = useState(false)
const wavesurfer = useWavesurfer(containerRef, props)
export const WaveSurferArtifactViewer: React.FC<WaveSurferArtifactViewerProps> =
(props) => {
const containerRef = useRef<HTMLDivElement>(null!)
const [isPlaying, setIsPlaying] = useState(false)
const wavesurfer = useWavesurfer(containerRef, props)
const onPlayClick = useCallback(() => {
if (!wavesurfer) return
wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play()
}, [wavesurfer])
const onPlayClick = useCallback(() => {
if (!wavesurfer) return
wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play()
}, [wavesurfer])
useEffect(() => {
if (!wavesurfer) return
useEffect(() => {
if (!wavesurfer) return
setIsPlaying(false)
setIsPlaying(false)
const subscriptions = [
wavesurfer.on("play", () => setIsPlaying(true)),
wavesurfer.on("pause", () => setIsPlaying(false)),
]
const subscriptions = [
wavesurfer.on("play", () => setIsPlaying(true)),
wavesurfer.on("pause", () => setIsPlaying(false)),
]
return () => {
subscriptions.forEach((unsub) => unsub())
}
}, [wavesurfer])
return () => {
subscriptions.forEach((unsub) => unsub())
}
}, [wavesurfer])
return (
<Box style={{ width: "100%", display: "flex", flexDirection: "column" }}>
<div ref={containerRef} style={{ minHeight: "120px", width: "100%" }} />
<button onClick={onPlayClick} style={{ marginTop: "1em" }}>
{isPlaying ? "Pause" : "Play"}
</button>
</Box>
)
}
return (
<Box style={{ width: "100%", display: "flex", flexDirection: "column" }}>
<div ref={containerRef} style={{ minHeight: "120px", width: "100%" }} />
<button onClick={onPlayClick} style={{ marginTop: "1em" }}>
{isPlaying ? "Pause" : "Play"}
</button>
</Box>
)
}
+4 -4
View File
@@ -71,8 +71,8 @@ function DataGrid<T>(props: {
initialRowsPerPage = initialRowsPerPage // use first element as default
? initialRowsPerPage
: isNumber(rowsPerPageOption[0])
? rowsPerPageOption[0]
: rowsPerPageOption[0].value
? rowsPerPageOption[0]
: rowsPerPageOption[0].value
const [rowsPerPage, setRowsPerPage] = React.useState(initialRowsPerPage)
const handleChangePage = (event: unknown, newPage: number) => {
@@ -321,8 +321,8 @@ function DataGridHeaderColumn<T>(props: {
filter === null
? filterChoices.filter((v) => v !== choice) // By default, every choice is ticked, so the chosen option will be unticked.
: filter.values.some((v) => v === choice)
? filter.values.filter((v) => v !== choice)
: [...filter.values, choice]
? filter.values.filter((v) => v !== choice)
: [...filter.values, choice]
onFilterChange(newTickedValues)
}}
>
@@ -11,7 +11,7 @@ import { actionCreator } from "../action"
export const useDeleteStudyDialog = (): [
(studyId: number) => void,
() => ReactNode
() => ReactNode,
] => {
const action = actionCreator()
@@ -15,27 +15,16 @@ import blue from "@mui/material/colors/blue"
import { useMergedUnionSearchSpace } from "../searchSpace"
import { usePlotlyColorTheme } from "../state"
import { getAxisInfo } from "../graphUtil"
import { useQuery } from "../urlQuery"
import { getPlotAPI, PlotType } from "../apiClient"
import { useRecoilValue } from "recoil"
import { plotlypyIsAvailableState } from "../state"
import { useBackendRender } from "../state"
const plotDomId = "graph-contour"
export const Contour: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const query = useQuery()
const plotlypyIsAvailable = useRecoilValue<boolean>(plotlypyIsAvailableState)
if (query.get("plotlypy_rendering") === "true") {
if (plotlypyIsAvailable) {
return <ContourBackend study={study} />
} else {
console.warn(
"Use frontend rendering because plotlypy is specified but not available."
)
return <ContourFrontend study={study} />
}
if (useBackendRender()) {
return <ContourBackend study={study} />
} else {
return <ContourFrontend study={study} />
}
@@ -45,6 +34,8 @@ const ContourBackend: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const studyId = study?.id
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
useEffect(() => {
if (studyId === undefined) {
return
@@ -56,7 +47,7 @@ const ContourBackend: FC<{
.catch((err) => {
console.error(err)
})
}, [studyId])
}, [studyId, numCompletedTrials])
return <Box id={plotDomId} sx={{ height: "450px" }} />
}
+38 -1
View File
@@ -2,7 +2,8 @@ import * as plotly from "plotly.js-dist-min"
import React, { FC, useEffect, useMemo } from "react"
import { Typography, useTheme, Box } from "@mui/material"
import { Target, useFilteredTrialsFromStudies } from "../trialFilter"
import { usePlotlyColorTheme } from "../state"
import { getCompareStudiesPlotAPI, CompareStudiesPlotType } from "../apiClient"
import { usePlotlyColorTheme, useBackendRender } from "../state"
const getPlotDomId = (objectiveId: number) => `graph-edf-${objectiveId}`
@@ -14,6 +15,42 @@ interface EdfPlotInfo {
export const GraphEdf: FC<{
studies: StudyDetail[]
objectiveId: number
}> = ({ studies, objectiveId }) => {
if (useBackendRender()) {
return <GraphEdfBackend studies={studies} />
} else {
return <GraphEdfFrontend studies={studies} objectiveId={objectiveId} />
}
}
const GraphEdfBackend: FC<{
studies: StudyDetail[]
}> = ({ studies }) => {
const studyIds = studies.map((s) => s.id)
const domId = getPlotDomId(-1)
const numCompletedTrials = studies.reduce(
(acc, study) =>
acc + study?.trials.filter((t) => t.state === "Complete").length,
0
)
useEffect(() => {
if (studyIds.length === 0) {
return
}
getCompareStudiesPlotAPI(studyIds, CompareStudiesPlotType.EDF)
.then(({ data, layout }) => {
plotly.react(domId, data, layout)
})
.catch((err) => {
console.error(err)
})
}, [studyIds, numCompletedTrials])
return <Box id={domId} sx={{ height: "450px" }} />
}
const GraphEdfFrontend: FC<{
studies: StudyDetail[]
objectiveId: number
}> = ({ studies, objectiveId }) => {
const theme = useTheme()
const colorTheme = usePlotlyColorTheme(theme.palette.mode)
+40 -12
View File
@@ -21,6 +21,7 @@ import {
useObjectiveAndUserAttrTargetsFromStudies,
} from "../trialFilter"
import { usePlotlyColorTheme } from "../state"
import { useNavigate } from "react-router-dom"
const plotDomId = "graph-history"
@@ -38,7 +39,7 @@ export const GraphHistory: FC<{
}> = ({ studies, logScale, includePruned }) => {
const theme = useTheme()
const colorTheme = usePlotlyColorTheme(theme.palette.mode)
const navigate = useNavigate()
const [xAxis, setXAxis] = useState<
"number" | "datetime_start" | "datetime_complete"
>("number")
@@ -72,15 +73,42 @@ export const GraphHistory: FC<{
colorTheme,
markerSize
)
}, [
studies,
selected,
logScale,
xAxis,
theme.palette.mode,
colorTheme,
markerSize,
])
const element = document.getElementById(plotDomId)
if (element !== null && studies.length >= 1) {
// @ts-ignore
element.on("plotly_click", (data) => {
if (data.points[0].data.mode !== "lines") {
let studyId = 1
if (data.points[0].data.name.includes("Infeasible Trial of")) {
const studyInfo: { id: number; name: string }[] = []
studies.forEach((study) => {
studyInfo.push({ id: study.id, name: study.name })
})
const dataPointStudyName = data.points[0].data.name.replace(
"Infeasible Trial of ",
""
)
const targetId = studyInfo.find(
(s) => s.name === dataPointStudyName
)?.id
if (targetId !== undefined) {
studyId = targetId
}
} else {
studyId = studies[Math.floor(data.points[0].curveNumber / 2)].id
}
navigate(
URL_PREFIX +
`/studies/${studyId}/trials?numbers=${data.points[0].x}`
)
}
})
return () => {
// @ts-ignore
element.removeAllListeners("plotly_click")
}
}
}, [studies, selected, logScale, xAxis, theme.palette.mode, colorTheme, markerSize])
const handleObjectiveChange = (event: SelectChangeEvent<string>) => {
setTarget(event.target.value)
@@ -226,8 +254,8 @@ const plotHistory = (
return xAxis === "number"
? trial.number
: xAxis === "datetime_start"
? trial.datetime_start ?? new Date()
: trial.datetime_complete ?? new Date()
? trial.datetime_start ?? new Date()
: trial.datetime_complete ?? new Date()
}
const plotData: Partial<plotly.PlotData>[] = []
@@ -90,8 +90,8 @@ const plotIntermediateValue = (
trial.state === "Running"
? "(running)"
: !isFeasible
? "(infeasible)"
: ""
? "(infeasible)"
: ""
}`,
...(!isFeasible && { line: { color: "#CCCCCC" } }),
}
@@ -17,6 +17,8 @@ import {
useParamTargets,
} from "../trialFilter"
import { useMergedUnionSearchSpace } from "../searchSpace"
import { getPlotAPI, PlotType } from "../apiClient"
import { useBackendRender } from "../state"
const plotDomId = "graph-parallel-coordinate"
@@ -86,6 +88,37 @@ const useTargets = (
export const GraphParallelCoordinate: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
if (useBackendRender()) {
return <GraphParallelCoordinateBackend study={study} />
} else {
return <GraphParallelCoordinateFrontend study={study} />
}
}
const GraphParallelCoordinateBackend: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const studyId = study?.id
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
useEffect(() => {
if (studyId === undefined) {
return
}
getPlotAPI(studyId, PlotType.ParallelCoordinate)
.then(({ data, layout }) => {
plotly.react(plotDomId, data, layout)
})
.catch((err) => {
console.error(err)
})
}, [studyId, numCompletedTrials])
return <Box id={plotDomId} sx={{ height: "450px" }} />
}
const GraphParallelCoordinateFrontend: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const theme = useTheme()
const colorTheme = usePlotlyColorTheme(theme.palette.mode)
@@ -13,6 +13,7 @@ import {
} from "@mui/material"
import { makeHovertext } from "../graphUtil"
import { usePlotlyColorTheme } from "../state"
import { useNavigate } from "react-router-dom"
const plotDomId = "graph-pareto-front"
@@ -21,7 +22,7 @@ export const GraphParetoFront: FC<{
}> = ({ study = null }) => {
const theme = useTheme()
const colorTheme = usePlotlyColorTheme(theme.palette.mode)
const navigate = useNavigate()
const [objectiveXId, setObjectiveXId] = useState<number>(0)
const [objectiveYId, setObjectiveYId] = useState<number>(1)
const objectiveNames: string[] = study?.objective_names || []
@@ -43,6 +44,23 @@ export const GraphParetoFront: FC<{
theme.palette.mode,
colorTheme
)
const element = document.getElementById(plotDomId)
if (element != null) {
// @ts-ignore
element.on("plotly_click", (data) => {
const plotTextInfo = JSON.parse(
data.points[0].text.replace(/<br>/g, "")
)
navigate(
URL_PREFIX +
`/studies/${study.id}/trials?numbers=${plotTextInfo.number}`
)
})
return () => {
// @ts-ignore
element.removeAllListeners("plotly_click")
}
}
}
}, [study, objectiveXId, objectiveYId, theme.palette.mode, colorTheme])
@@ -196,12 +214,12 @@ const getIsDominated2D = (normalizedValues: number[][]) => {
a[0] > b[0]
? 1
: a[0] < b[0]
? -1
: a[1] > b[1]
? 1
: a[1] < b[1]
? -1
: 0
? -1
: a[1] > b[1]
? 1
: a[1] < b[1]
? -1
: 0
)
let maxValueSeen0 = sorted[0][0]
let minValueSeen1 = sorted[0][1]
+35 -3
View File
@@ -13,7 +13,8 @@ import {
} from "@mui/material"
import { getAxisInfo, makeHovertext } from "../graphUtil"
import { useMergedUnionSearchSpace } from "../searchSpace"
import { usePlotlyColorTheme } from "../state"
import { usePlotlyColorTheme, useBackendRender } from "../state"
import { getPlotAPI, PlotType } from "../apiClient"
const plotDomId = "graph-rank"
@@ -31,6 +32,37 @@ interface RankPlotInfo {
export const GraphRank: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
if (useBackendRender()) {
return <GraphRankBackend study={study} />
} else {
return <GraphRankFrontend study={study} />
}
}
const GraphRankBackend: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const studyId = study?.id
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
useEffect(() => {
if (studyId === undefined) {
return
}
getPlotAPI(studyId, PlotType.Rank)
.then(({ data, layout }) => {
plotly.react(plotDomId, data, layout)
})
.catch((err) => {
console.error(err)
})
}, [studyId, numCompletedTrials])
return <Box id={plotDomId} sx={{ height: "450px" }} />
}
const GraphRankFrontend: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const theme = useTheme()
const colorTheme = usePlotlyColorTheme(theme.palette.mode)
@@ -159,8 +191,8 @@ const getRankPlotInfo = (
return typeof value === "number"
? value
: value.includes("-")
? -Infinity
: Infinity
? -Infinity
: Infinity
}
filteredTrials.forEach((trial, i) => {
const xValue = xAxis.values[i]
+33 -1
View File
@@ -19,7 +19,8 @@ import {
useParamTargets,
} from "../trialFilter"
import { useMergedUnionSearchSpace } from "../searchSpace"
import { usePlotlyColorTheme } from "../state"
import { usePlotlyColorTheme, useBackendRender } from "../state"
import { getPlotAPI, PlotType } from "../apiClient"
const plotDomId = "graph-slice"
@@ -32,6 +33,37 @@ const isLogScale = (s: SearchSpaceItem): boolean => {
export const GraphSlice: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
if (useBackendRender()) {
return <GraphSliceBackend study={study} />
} else {
return <GraphSliceFrontend study={study} />
}
}
const GraphSliceBackend: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const studyId = study?.id
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
useEffect(() => {
if (studyId === undefined) {
return
}
getPlotAPI(studyId, PlotType.Slice)
.then(({ data, layout }) => {
plotly.react(plotDomId, data, layout)
})
.catch((err) => {
console.error(err)
})
}, [studyId, numCompletedTrials])
return <Box id={plotDomId} sx={{ height: "450px" }} />
}
const GraphSliceFrontend: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const theme = useTheme()
const colorTheme = usePlotlyColorTheme(theme.palette.mode)
+9 -28
View File
@@ -12,11 +12,11 @@ import {
import Grid2 from "@mui/material/Unstable_Grid2"
import ChevronRightIcon from "@mui/icons-material/ChevronRight"
import HomeIcon from "@mui/icons-material/Home"
import DownloadIcon from "@mui/icons-material/Download"
import { StudyNote } from "./Note"
import { actionCreator } from "../action"
import {
fetchedTrialsPartiallyState,
reloadIntervalState,
useStudyDetailValue,
useStudyIsPreferential,
@@ -57,6 +57,9 @@ export const StudyDetail: FC<{
const reloadInterval = useRecoilValue<number>(reloadIntervalState)
const studyName = useStudyName(studyId)
const isPreferential = useStudyIsPreferential(studyId)
const fetchedTrialsPartially = useRecoilValue<boolean>(
fetchedTrialsPartiallyState
)
const title =
studyName !== null ? `${studyName} (id=${studyId})` : `Study #${studyId}`
@@ -73,9 +76,13 @@ export const StudyDetail: FC<{
const nTrials = studyDetail ? studyDetail.trials.length : 0
let interval = reloadInterval * 1000
// If trials are left in cache, we collect them quickly.
// For Human-in-the-loop Optimization, the interval is set to 2 seconds
// when the number of trials is small, and the page is "trialList" or top page of preferential.
if (
if (fetchedTrialsPartially) {
// Too short time is frustrating because the page freezes until the rendering is done.
interval = 3000
} else if (
(!isPreferential && page === "trialList") ||
(isPreferential && page === "top")
) {
@@ -150,32 +157,6 @@ export const StudyDetail: FC<{
} else if (page === "trialTable") {
content = (
<Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}>
<Card
sx={{
margin: theme.spacing(2),
width: "auto",
height: "auto",
display: "flex",
justifyContent: "left",
alignItems: "left",
}}
>
<CardContent>
<IconButton
aria-label="download csv"
size="small"
color="inherit"
download
sx={{ margin: "auto 0" }}
href={`/csv/${studyDetail?.id}`}
>
<DownloadIcon />
<Typography variant="button" sx={{ margin: theme.spacing(2) }}>
Download CSV File
</Typography>
</IconButton>
</CardContent>
</Card>
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<TrialTable studyDetail={studyDetail} initialRowsPerPage={50} />
@@ -42,8 +42,8 @@ export const TrialFormWidgets: FC<{
formWidgets.output_type === "user_attr"
? "Set User Attributes Form"
: directions.length > 1
? "Set Objective Values Form"
: "Set Objective Value Form"
? "Set Objective Values Form"
: "Set Objective Value Form"
const widgetNames = formWidgets.widgets.map((widget, i) => {
if (formWidgets.output_type === "objective") {
if (objectiveNames.at(i) !== undefined) {
+21 -8
View File
@@ -1,6 +1,7 @@
import React, { FC } from "react"
import { IconButton } from "@mui/material"
import { IconButton, Button, useTheme } from "@mui/material"
import LinkIcon from "@mui/icons-material/Link"
import DownloadIcon from "@mui/icons-material/Download"
import { DataGridColumn, DataGrid } from "./DataGrid"
import { Link } from "react-router-dom"
@@ -9,6 +10,7 @@ export const TrialTable: FC<{
studyDetail: StudyDetail | null
initialRowsPerPage?: number
}> = ({ studyDetail, initialRowsPerPage }) => {
const theme = useTheme()
const trials: Trial[] = studyDetail !== null ? studyDetail.trials : []
const objectiveNames: string[] = studyDetail?.objective_names || []
@@ -190,12 +192,23 @@ export const TrialTable: FC<{
})
return (
<DataGrid<Trial>
columns={columns}
rows={trials}
keyField={"trial_id"}
dense={true}
initialRowsPerPage={initialRowsPerPage}
/>
<>
<DataGrid<Trial>
columns={columns}
rows={trials}
keyField={"trial_id"}
dense={true}
initialRowsPerPage={initialRowsPerPage}
/>
<Button
variant="outlined"
startIcon={<DownloadIcon />}
download
href={`/csv/${studyDetail?.id}`}
sx={{ marginRight: theme.spacing(2), minWidth: "120px" }}
>
Download CSV File
</Button>
</>
)
}
+2 -2
View File
@@ -47,8 +47,8 @@ const getAxisInfoForCategoricalParams = (
a.toLowerCase() < b.toLowerCase()
? -1
: a.toLowerCase() > b.toLowerCase()
? 1
: 0
? 1
: 0
)
return {
name: paramName,
+21
View File
@@ -3,6 +3,7 @@ import {
LightColorTemplates,
DarkColorTemplates,
} from "./components/PlotlyColorTemplates"
import { useQuery } from "./urlQuery"
export const studySummariesState = atom<StudySummary[]>({
key: "studySummaries",
@@ -37,6 +38,11 @@ export const drawerOpenState = atom<boolean>({
default: false,
})
export const fetchedTrialsPartiallyState = atom<boolean>({
key: "fetchedTrialsPartially",
default: false,
})
export const isFileUploading = atom<boolean>({
key: "isFileUploading",
default: false,
@@ -125,3 +131,18 @@ export const usePlotlyColorTheme = (mode: string): Partial<Plotly.Template> => {
return LightColorTemplates[theme.light]
}
}
export const useBackendRender = (): boolean => {
const query = useQuery()
const plotlypyIsAvailable = useRecoilValue<boolean>(plotlypyIsAvailableState)
if (query.get("plotlypy_rendering") === "true") {
if (plotlypyIsAvailable) {
return true
}
console.warn(
"Use frontend rendering because plotlypy is specified but not available."
)
}
return false
}
+1
View File
@@ -220,6 +220,7 @@ type StudyDetail = {
plotly_graph_objects: PlotlyGraphObject[]
artifacts: Artifact[]
skipped_trial_numbers: number[]
fetched_trials_partially: boolean
}
type StudyDetails = {
+228 -22
View File
@@ -38,6 +38,7 @@
"devDependencies": {
"@babel/core": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"@biomejs/biome": "1.5.3",
"@testing-library/react": "^14.1.2",
"@types/jest": "^29.5.11",
"@types/plotly.js": "^2.12.32",
@@ -53,7 +54,6 @@
"jest": "^29.7.0",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.7.0",
"prettier": "^2.5.1",
"style-loader": "^3.3.4",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
@@ -1818,6 +1818,161 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"node_modules/@biomejs/biome": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.5.3.tgz",
"integrity": "sha512-yvZCa/g3akwTaAQ7PCwPWDCkZs3Qa5ONg/fgOUT9e6wAWsPftCjLQFPXBeGxPK30yZSSpgEmRCfpGTmVbUjGgg==",
"dev": true,
"hasInstallScript": true,
"bin": {
"biome": "bin/biome"
},
"engines": {
"node": ">=14.*"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "1.5.3",
"@biomejs/cli-darwin-x64": "1.5.3",
"@biomejs/cli-linux-arm64": "1.5.3",
"@biomejs/cli-linux-arm64-musl": "1.5.3",
"@biomejs/cli-linux-x64": "1.5.3",
"@biomejs/cli-linux-x64-musl": "1.5.3",
"@biomejs/cli-win32-arm64": "1.5.3",
"@biomejs/cli-win32-x64": "1.5.3"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.5.3.tgz",
"integrity": "sha512-ImU7mh1HghEDyqNmxEZBoMPr8SxekkZuYcs+gynKlNW+TALQs7swkERiBLkG9NR0K1B3/2uVzlvYowXrmlW8hw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.*"
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.5.3.tgz",
"integrity": "sha512-vCdASqYnlpq/swErH7FD6nrFz0czFtK4k/iLgj0/+VmZVjineFPgevOb+Sr9vz0tk0GfdQO60bSpI74zU8M9Dw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.*"
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.5.3.tgz",
"integrity": "sha512-cupBQv0sNF1OKqBfx7EDWMSsKwRrBUZfjXawT4s6hKV6ALq7p0QzWlxr/sDmbKMLOaLQtw2Qgu/77N9rm+f9Rg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.*"
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.5.3.tgz",
"integrity": "sha512-DYuMizUYUBYfS0IHGjDrOP1RGipqWfMGEvNEJ398zdtmCKLXaUvTimiox5dvx4X15mBK5M2m8wgWUgOP1giUpQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.*"
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.5.3.tgz",
"integrity": "sha512-YQrSArQvcv4FYsk7Q91Yv4uuu5F8hJyORVcv3zsjCLGkjIjx2RhjYLpTL733SNL7v33GmOlZY0eFR1ko38tuUw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.*"
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.5.3.tgz",
"integrity": "sha512-UUHiAnlDqr2Y/LpvshBFhUYMWkl2/Jn+bi3U6jKuav0qWbbBKU/ByHgR4+NBxpKBYoCtWxhnmatfH1bpPIuZMw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.*"
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.5.3.tgz",
"integrity": "sha512-HxatYH7vf/kX9nrD+pDYuV2GI9GV8EFo6cfKkahAecTuZLPxryHx1WEfJthp5eNsE0+09STGkKIKjirP0ufaZA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.*"
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.5.3.tgz",
"integrity": "sha512-fMvbSouZEASU7mZH8SIJSANDm5OqsjgtVXlbUqxwed6BP7uuHRSs396Aqwh2+VoW8fwTpp6ybIUoC9FrzB0kyA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.*"
}
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
@@ -12830,21 +12985,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
@@ -16840,6 +16980,78 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"@biomejs/biome": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.5.3.tgz",
"integrity": "sha512-yvZCa/g3akwTaAQ7PCwPWDCkZs3Qa5ONg/fgOUT9e6wAWsPftCjLQFPXBeGxPK30yZSSpgEmRCfpGTmVbUjGgg==",
"dev": true,
"requires": {
"@biomejs/cli-darwin-arm64": "1.5.3",
"@biomejs/cli-darwin-x64": "1.5.3",
"@biomejs/cli-linux-arm64": "1.5.3",
"@biomejs/cli-linux-arm64-musl": "1.5.3",
"@biomejs/cli-linux-x64": "1.5.3",
"@biomejs/cli-linux-x64-musl": "1.5.3",
"@biomejs/cli-win32-arm64": "1.5.3",
"@biomejs/cli-win32-x64": "1.5.3"
}
},
"@biomejs/cli-darwin-arm64": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.5.3.tgz",
"integrity": "sha512-ImU7mh1HghEDyqNmxEZBoMPr8SxekkZuYcs+gynKlNW+TALQs7swkERiBLkG9NR0K1B3/2uVzlvYowXrmlW8hw==",
"dev": true,
"optional": true
},
"@biomejs/cli-darwin-x64": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.5.3.tgz",
"integrity": "sha512-vCdASqYnlpq/swErH7FD6nrFz0czFtK4k/iLgj0/+VmZVjineFPgevOb+Sr9vz0tk0GfdQO60bSpI74zU8M9Dw==",
"dev": true,
"optional": true
},
"@biomejs/cli-linux-arm64": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.5.3.tgz",
"integrity": "sha512-cupBQv0sNF1OKqBfx7EDWMSsKwRrBUZfjXawT4s6hKV6ALq7p0QzWlxr/sDmbKMLOaLQtw2Qgu/77N9rm+f9Rg==",
"dev": true,
"optional": true
},
"@biomejs/cli-linux-arm64-musl": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.5.3.tgz",
"integrity": "sha512-DYuMizUYUBYfS0IHGjDrOP1RGipqWfMGEvNEJ398zdtmCKLXaUvTimiox5dvx4X15mBK5M2m8wgWUgOP1giUpQ==",
"dev": true,
"optional": true
},
"@biomejs/cli-linux-x64": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.5.3.tgz",
"integrity": "sha512-YQrSArQvcv4FYsk7Q91Yv4uuu5F8hJyORVcv3zsjCLGkjIjx2RhjYLpTL733SNL7v33GmOlZY0eFR1ko38tuUw==",
"dev": true,
"optional": true
},
"@biomejs/cli-linux-x64-musl": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.5.3.tgz",
"integrity": "sha512-UUHiAnlDqr2Y/LpvshBFhUYMWkl2/Jn+bi3U6jKuav0qWbbBKU/ByHgR4+NBxpKBYoCtWxhnmatfH1bpPIuZMw==",
"dev": true,
"optional": true
},
"@biomejs/cli-win32-arm64": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.5.3.tgz",
"integrity": "sha512-HxatYH7vf/kX9nrD+pDYuV2GI9GV8EFo6cfKkahAecTuZLPxryHx1WEfJthp5eNsE0+09STGkKIKjirP0ufaZA==",
"dev": true,
"optional": true
},
"@biomejs/cli-win32-x64": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.5.3.tgz",
"integrity": "sha512-fMvbSouZEASU7mZH8SIJSANDm5OqsjgtVXlbUqxwed6BP7uuHRSs396Aqwh2+VoW8fwTpp6ybIUoC9FrzB0kyA==",
"dev": true,
"optional": true
},
"@discoveryjs/json-ext": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
@@ -24733,12 +24945,6 @@
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true
},
"prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true
},
"pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+3 -3
View File
@@ -5,10 +5,10 @@
"description": "Dashboard for Optuna",
"main": "index.js",
"scripts": {
"fmt": "prettier --write \"{optuna_dashboard/ts,typescript_tests,standalone_app/src,vscode/src}/**/*.{ts,tsx}\"",
"fmt": "biome format --write .",
"lint": "npm run lint:eslint && npm run lint:fmt",
"lint:eslint": "eslint . --ext .ts,.tsx --max-warnings 0",
"lint:fmt": "prettier --list-different \"{optuna_dashboard/ts,typescript_tests,standalone_app/src,vscode/src}/**/*.{ts,tsx}\"",
"lint:fmt": "biome format .",
"watch": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack --watch",
"build": "webpack",
"build:dev": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack",
@@ -47,6 +47,7 @@
"devDependencies": {
"@babel/core": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"@biomejs/biome": "1.5.3",
"@testing-library/react": "^14.1.2",
"@types/jest": "^29.5.11",
"@types/plotly.js": "^2.12.32",
@@ -62,7 +63,6 @@
"jest": "^29.7.0",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.7.0",
"prettier": "^2.5.1",
"style-loader": "^3.3.4",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
+29 -45
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import json
import sys
from typing import Any
from unittest import TestCase
import optuna
@@ -45,70 +46,53 @@ class APITestCase(TestCase):
study_summaries = json.loads(body)["study_summaries"]
self.assertEqual(len(study_summaries), 2)
def test_get_study_details_without_after_param(self) -> None:
def run_get_study_details(
self,
queries: dict[str, str] | None = None,
expected_status: int = 200,
) -> list[dict[str, Any]]:
study = optuna.create_study()
study_id = study._study_id
study.optimize(objective, n_trials=2)
study.optimize(objective, n_trials=10)
app = create_app(study._storage)
status, _, body = send_request(
app,
f"/api/studies/{study_id}",
"GET",
queries=queries,
content_type="application/json",
)
self.assertEqual(status, 200)
all_trials = json.loads(body)["trials"]
self.assertEqual(len(all_trials), 2)
self.assertEqual(status, expected_status)
if expected_status == 400:
return []
else:
return json.loads(body)["trials"]
def test_get_study_details_without_after_param(self) -> None:
all_trials = self.run_get_study_details()
self.assertEqual(len(all_trials), 10)
def test_get_study_details_with_after_param_partial(self) -> None:
study = optuna.create_study()
study_id = study._study_id
study.optimize(objective, n_trials=2)
app = create_app(study._storage)
all_trials = self.run_get_study_details({"after": "5"})
self.assertEqual(len(all_trials), 5)
status, _, body = send_request(
app,
f"/api/studies/{study_id}",
"GET",
queries={"after": "1"},
content_type="application/json",
)
self.assertEqual(status, 200)
all_trials = json.loads(body)["trials"]
self.assertEqual(len(all_trials), 1)
def test_get_study_details_with_params(self) -> None:
for after in [0, 5, 9, 10]:
for limit in [1, 2, 5, 10]:
trials = self.run_get_study_details({"after": str(after), "limit": str(limit)})
ans = list(range(after, min(10, after + limit)))
self.assertEqual([t["number"] for t in trials], ans)
def test_get_study_details_with_after_param_full(self) -> None:
study = optuna.create_study()
study_id = study._study_id
study.optimize(objective, n_trials=2)
app = create_app(study._storage)
status, _, body = send_request(
app,
f"/api/studies/{study_id}",
"GET",
queries={"after": "2"},
content_type="application/json",
)
self.assertEqual(status, 200)
all_trials = json.loads(body)["trials"]
all_trials = self.run_get_study_details({"after": "10"})
self.assertEqual(len(all_trials), 0)
def test_get_study_details_with_after_param_illegal(self) -> None:
study = optuna.create_study()
study_id = study._study_id
study.optimize(objective, n_trials=2)
app = create_app(study._storage)
self.run_get_study_details({"after": "-1"}, expected_status=400)
status, _, body = send_request(
app,
f"/api/studies/{study_id}",
"GET",
queries={"after": "-1"},
content_type="application/json",
)
self.assertEqual(status, 400)
def test_get_study_details_with_limit_param_illegal(self) -> None:
self.run_get_study_details({"limit": "-1"}, expected_status=400)
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
@pytest.mark.skipif(
@@ -132,7 +132,7 @@ class _CachedExtraStudyPropertySearchSpaceTestCase(TestCase):
create_trial(
state=TrialState.COMPLETE, value=0, distributions=distributions, params=params
),
create_trial(state=TrialState.FAIL, value=0, distributions={}, params={}),
create_trial(state=TrialState.FAIL, value=None, distributions={}, params={}),
create_trial(
state=TrialState.COMPLETE, value=0, distributions=distributions, params=params
),
@@ -235,7 +235,7 @@ class _CachedExtraStudyPropertyUserAttrs(TestCase):
),
create_trial(
state=TrialState.FAIL,
value=0,
value=None,
distributions={},
params={},
user_attrs={"bar": "bar"},
+2 -2
View File
@@ -65,7 +65,7 @@ def test_get_study_detail_is_preferential() -> None:
study_summary = study_summaries[0]
study_detail = serialize_study_detail(
study_summary, [], study.trials, [], [], [], False, {}, []
study_summary, [], study.trials, [], [], [], False, {}, [], False
)
assert study_detail["is_preferential"]
@@ -78,7 +78,7 @@ def test_get_study_detail_is_not_preferential() -> None:
study_summary = study_summaries[0]
study_detail = serialize_study_detail(
study_summary, [], study.trials, [], [], [], False, {}, []
study_summary, [], study.trials, [], [], [], False, {}, [], False
)
assert not study_detail["is_preferential"]
+3
View File
@@ -1,4 +1,7 @@
[flake8]
ignore =
E203
W503
max-line-length = 99
statistics = True
exclude = venv,build
+2 -2
View File
@@ -65,8 +65,8 @@ function DataGrid<T>(props: {
initialRowsPerPage = initialRowsPerPage // use first element as default
? initialRowsPerPage
: isNumber(rowsPerPageOption[0])
? rowsPerPageOption[0]
: rowsPerPageOption[0].value
? rowsPerPageOption[0]
: rowsPerPageOption[0].value
const [rowsPerPage, setRowsPerPage] = React.useState(initialRowsPerPage)
const handleChangePage = (event: unknown, newPage: number) => {
@@ -231,8 +231,8 @@ const plotHistory = (
return xAxis === "number"
? trial.number
: xAxis === "datetime_start"
? trial.datetime_start ?? new Date()
: trial.datetime_complete ?? new Date()
? trial.datetime_start ?? new Date()
: trial.datetime_complete ?? new Date()
}
const getValue = (trial: Trial, objectiveId: number): number | null => {
+12 -12
View File
@@ -174,12 +174,12 @@ const getTrials = (
vals[2] === "COMPLETE"
? "Complete"
: vals[2] === "PRUNED"
? "Pruned"
: vals[2] === "RUNNING"
? "Running"
: vals[2] === "WAITING"
? "Waiting"
: "Fail"
? "Pruned"
: vals[2] === "RUNNING"
? "Running"
: vals[2] === "WAITING"
? "Waiting"
: "Fail"
const trial: Trial = {
trial_id: trialId,
number: vals[1],
@@ -220,8 +220,8 @@ const getTrialValues = (
vals[1] === "INF_NEG"
? "-inf"
: vals[1] === "INF_POS"
? "+inf"
: vals[0]
? "+inf"
: vals[0]
)
},
})
@@ -391,10 +391,10 @@ const getTrialIntermediateValues = (
vals[2] === "INF_NEG"
? "-inf"
: vals[2] === "INF_POS"
? "+inf"
: vals[2] === "NAN"
? "nan"
: vals[1],
? "+inf"
: vals[2] === "NAN"
? "nan"
: vals[1],
})
},
})