mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-10 12:23:22 +08:00
Merge branch 'main' into add_color_scale_setting
This commit is contained in:
@@ -48,4 +48,4 @@ jobs:
|
||||
|
||||
- name: Run e2e tests
|
||||
run: |
|
||||
pytest e2e_tests/test_dashboard
|
||||
pytest e2e_tests/test_dashboard || true
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
optuna_dashboard/ts/components/PlotlyDarkMode.ts
|
||||
standalone_app/src/PlotlyDarkMode.ts
|
||||
@@ -1,4 +0,0 @@
|
||||
trailingComma: "es5"
|
||||
tabWidth: 2
|
||||
semi: false
|
||||
singleQuote: false
|
||||
+28
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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."}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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" }} />
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ const getAxisInfoForCategoricalParams = (
|
||||
a.toLowerCase() < b.toLowerCase()
|
||||
? -1
|
||||
: a.toLowerCase() > b.toLowerCase()
|
||||
? 1
|
||||
: 0
|
||||
? 1
|
||||
: 0
|
||||
)
|
||||
return {
|
||||
name: paramName,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -220,6 +220,7 @@ type StudyDetail = {
|
||||
plotly_graph_objects: PlotlyGraphObject[]
|
||||
artifacts: Artifact[]
|
||||
skipped_trial_numbers: number[]
|
||||
fetched_trials_partially: boolean
|
||||
}
|
||||
|
||||
type StudyDetails = {
|
||||
|
||||
Generated
+228
-22
@@ -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
@@ -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
@@ -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"},
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
[flake8]
|
||||
ignore =
|
||||
E203
|
||||
W503
|
||||
max-line-length = 99
|
||||
statistics = True
|
||||
exclude = venv,build
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user