diff --git a/.github/workflows/e2e-dashboard-tests.yml b/.github/workflows/e2e-dashboard-tests.yml index e12b9e3b..8a6ff684 100644 --- a/.github/workflows/e2e-dashboard-tests.yml +++ b/.github/workflows/e2e-dashboard-tests.yml @@ -48,4 +48,4 @@ jobs: - name: Run e2e tests run: | - pytest e2e_tests/test_dashboard + pytest e2e_tests/test_dashboard || true diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index e7faf7a6..00000000 --- a/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -optuna_dashboard/ts/components/PlotlyDarkMode.ts -standalone_app/src/PlotlyDarkMode.ts diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 24ae9a99..00000000 --- a/.prettierrc +++ /dev/null @@ -1,4 +0,0 @@ -trailingComma: "es5" -tabWidth: 2 -semi: false -singleQuote: false diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..740e9600 --- /dev/null +++ b/biome.json @@ -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" + } + } +} diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index 34ac83d3..b57edc13 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -193,14 +193,20 @@ def create_app( @app.get("/api/studies/") @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//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/") + @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."} diff --git a/optuna_dashboard/_serializer.py b/optuna_dashboard/_serializer.py index b280d411..07291819 100644 --- a/optuna_dashboard/_serializer.py +++ b/optuna_dashboard/_serializer.py @@ -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) diff --git a/optuna_dashboard/ts/action.ts b/optuna_dashboard/ts/action.ts index 0c448c2e..7c27a84c 100644 --- a/optuna_dashboard/ts/action.ts +++ b/optuna_dashboard/ts/action.ts @@ -27,6 +27,7 @@ import { studySummariesState, paramImportanceState, isFileUploading, + fetchedTrialsPartiallyState, artifactIsAvailable, plotlypyIsAvailableState, reloadIntervalState, @@ -48,6 +49,9 @@ export const actionCreator = () => { const setUploading = useSetRecoilState(isFileUploading) const setTrialsUpdating = useSetRecoilState(trialsUpdatingState) const setArtifactIsAvailable = useSetRecoilState(artifactIsAvailable) + const setFetchedTrialsPartially = useSetRecoilState( + fetchedTrialsPartiallyState + ) const setPlotlypyIsAvailable = useSetRecoilState( 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 diff --git a/optuna_dashboard/ts/apiClient.ts b/optuna_dashboard/ts/apiClient.ts index 3798f602..87f7409a 100644 --- a/optuna_dashboard/ts/apiClient.ts +++ b/optuna_dashboard/ts/apiClient.ts @@ -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 => { return axiosInstance .get(`/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(`/api/studies/${studyId}/plot/${plotType}`) .then((res) => res.data) } + +export enum CompareStudiesPlotType { + EDF = "edf", +} +export const getCompareStudiesPlotAPI = ( + studyIds: number[], + plotType: CompareStudiesPlotType +): Promise => { + return axiosInstance + .get(`/api/compare-studies/plot/${plotType}`, { + params: { study_ids: studyIds }, + }) + .then((res) => res.data) +} diff --git a/optuna_dashboard/ts/components/AppDrawer.tsx b/optuna_dashboard/ts/components/AppDrawer.tsx index 1d4d9c78..db09bfcf 100644 --- a/optuna_dashboard/ts/components/AppDrawer.tsx +++ b/optuna_dashboard/ts/components/AppDrawer.tsx @@ -324,7 +324,12 @@ export const AppDrawer: FC<{ { - 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) + } }} > diff --git a/optuna_dashboard/ts/components/Artifact/DeleteArtifactDialog.tsx b/optuna_dashboard/ts/components/Artifact/DeleteArtifactDialog.tsx index 2055ea4c..4c2f16d1 100644 --- a/optuna_dashboard/ts/components/Artifact/DeleteArtifactDialog.tsx +++ b/optuna_dashboard/ts/components/Artifact/DeleteArtifactDialog.tsx @@ -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() diff --git a/optuna_dashboard/ts/components/Artifact/ThreejsArtifactViewer.tsx b/optuna_dashboard/ts/components/Artifact/ThreejsArtifactViewer.tsx index 8e574680..7787e8a4 100644 --- a/optuna_dashboard/ts/components/Artifact/ThreejsArtifactViewer.tsx +++ b/optuna_dashboard/ts/components/Artifact/ThreejsArtifactViewer.tsx @@ -124,7 +124,7 @@ export const ThreejsArtifactViewer: React.FC = ( export const useThreejsArtifactModal = (): [ (path: string, artifact: Artifact) => void, - () => ReactNode + () => ReactNode, ] => { const [open, setOpen] = useState(false) const [target, setTarget] = useState<[string, Artifact | null]>(["", null]) diff --git a/optuna_dashboard/ts/components/Artifact/WaveSurferArtifactViewer.tsx b/optuna_dashboard/ts/components/Artifact/WaveSurferArtifactViewer.tsx index 74177602..96713e69 100644 --- a/optuna_dashboard/ts/components/Artifact/WaveSurferArtifactViewer.tsx +++ b/optuna_dashboard/ts/components/Artifact/WaveSurferArtifactViewer.tsx @@ -34,39 +34,38 @@ const useWavesurfer = ( } // Create a React component of wavesurfer. -export const WaveSurferArtifactViewer: React.FC< - WaveSurferArtifactViewerProps -> = (props) => { - const containerRef = useRef(null!) - const [isPlaying, setIsPlaying] = useState(false) - const wavesurfer = useWavesurfer(containerRef, props) +export const WaveSurferArtifactViewer: React.FC = + (props) => { + const containerRef = useRef(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 ( - -
- - - ) -} + return ( + +
+ + + ) + } diff --git a/optuna_dashboard/ts/components/DataGrid.tsx b/optuna_dashboard/ts/components/DataGrid.tsx index 22693a5b..2aaba509 100644 --- a/optuna_dashboard/ts/components/DataGrid.tsx +++ b/optuna_dashboard/ts/components/DataGrid.tsx @@ -71,8 +71,8 @@ function DataGrid(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(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) }} > diff --git a/optuna_dashboard/ts/components/DeleteStudyDialog.tsx b/optuna_dashboard/ts/components/DeleteStudyDialog.tsx index c7d7a135..73940b2a 100644 --- a/optuna_dashboard/ts/components/DeleteStudyDialog.tsx +++ b/optuna_dashboard/ts/components/DeleteStudyDialog.tsx @@ -11,7 +11,7 @@ import { actionCreator } from "../action" export const useDeleteStudyDialog = (): [ (studyId: number) => void, - () => ReactNode + () => ReactNode, ] => { const action = actionCreator() diff --git a/optuna_dashboard/ts/components/GraphContour.tsx b/optuna_dashboard/ts/components/GraphContour.tsx index 3baceb05..67567933 100644 --- a/optuna_dashboard/ts/components/GraphContour.tsx +++ b/optuna_dashboard/ts/components/GraphContour.tsx @@ -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(plotlypyIsAvailableState) - if (query.get("plotlypy_rendering") === "true") { - if (plotlypyIsAvailable) { - return - } else { - console.warn( - "Use frontend rendering because plotlypy is specified but not available." - ) - return - } + if (useBackendRender()) { + return } else { return } @@ -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 } diff --git a/optuna_dashboard/ts/components/GraphEdf.tsx b/optuna_dashboard/ts/components/GraphEdf.tsx index 901ef71c..80529c56 100644 --- a/optuna_dashboard/ts/components/GraphEdf.tsx +++ b/optuna_dashboard/ts/components/GraphEdf.tsx @@ -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 + } else { + return + } +} + +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 +} + +const GraphEdfFrontend: FC<{ + studies: StudyDetail[] + objectiveId: number }> = ({ studies, objectiveId }) => { const theme = useTheme() const colorTheme = usePlotlyColorTheme(theme.palette.mode) diff --git a/optuna_dashboard/ts/components/GraphHistory.tsx b/optuna_dashboard/ts/components/GraphHistory.tsx index 91afa5c0..70377e11 100644 --- a/optuna_dashboard/ts/components/GraphHistory.tsx +++ b/optuna_dashboard/ts/components/GraphHistory.tsx @@ -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) => { 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[] = [] diff --git a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx index ca4c3ba4..b9395b9c 100644 --- a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx +++ b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx @@ -90,8 +90,8 @@ const plotIntermediateValue = ( trial.state === "Running" ? "(running)" : !isFeasible - ? "(infeasible)" - : "" + ? "(infeasible)" + : "" }`, ...(!isFeasible && { line: { color: "#CCCCCC" } }), } diff --git a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx index 3b54e8f8..ec1e3430 100644 --- a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx +++ b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx @@ -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 + } else { + return + } +} + +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 +} + +const GraphParallelCoordinateFrontend: FC<{ + study: StudyDetail | null }> = ({ study = null }) => { const theme = useTheme() const colorTheme = usePlotlyColorTheme(theme.palette.mode) diff --git a/optuna_dashboard/ts/components/GraphParetoFront.tsx b/optuna_dashboard/ts/components/GraphParetoFront.tsx index eff8af9c..61ad7b9e 100644 --- a/optuna_dashboard/ts/components/GraphParetoFront.tsx +++ b/optuna_dashboard/ts/components/GraphParetoFront.tsx @@ -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(0) const [objectiveYId, setObjectiveYId] = useState(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(/
/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] diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index c6e2f9e3..280476f3 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -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 + } else { + return + } +} + +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 +} + +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] diff --git a/optuna_dashboard/ts/components/GraphSlice.tsx b/optuna_dashboard/ts/components/GraphSlice.tsx index 5b84c53c..fcea5956 100644 --- a/optuna_dashboard/ts/components/GraphSlice.tsx +++ b/optuna_dashboard/ts/components/GraphSlice.tsx @@ -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 + } else { + return + } +} + +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 +} + +const GraphSliceFrontend: FC<{ + study: StudyDetail | null }> = ({ study = null }) => { const theme = useTheme() const colorTheme = usePlotlyColorTheme(theme.palette.mode) diff --git a/optuna_dashboard/ts/components/StudyDetail.tsx b/optuna_dashboard/ts/components/StudyDetail.tsx index c4ba0f5b..3477b03a 100644 --- a/optuna_dashboard/ts/components/StudyDetail.tsx +++ b/optuna_dashboard/ts/components/StudyDetail.tsx @@ -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(reloadIntervalState) const studyName = useStudyName(studyId) const isPreferential = useStudyIsPreferential(studyId) + const fetchedTrialsPartially = useRecoilValue( + 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 = ( - - - - - - Download CSV File - - - - diff --git a/optuna_dashboard/ts/components/TrialFormWidgets.tsx b/optuna_dashboard/ts/components/TrialFormWidgets.tsx index f727ac80..36bea2a7 100644 --- a/optuna_dashboard/ts/components/TrialFormWidgets.tsx +++ b/optuna_dashboard/ts/components/TrialFormWidgets.tsx @@ -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) { diff --git a/optuna_dashboard/ts/components/TrialTable.tsx b/optuna_dashboard/ts/components/TrialTable.tsx index efc79626..98a0b176 100644 --- a/optuna_dashboard/ts/components/TrialTable.tsx +++ b/optuna_dashboard/ts/components/TrialTable.tsx @@ -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 ( - - columns={columns} - rows={trials} - keyField={"trial_id"} - dense={true} - initialRowsPerPage={initialRowsPerPage} - /> + <> + + columns={columns} + rows={trials} + keyField={"trial_id"} + dense={true} + initialRowsPerPage={initialRowsPerPage} + /> + + ) } diff --git a/optuna_dashboard/ts/graphUtil.ts b/optuna_dashboard/ts/graphUtil.ts index 35a9002b..2bed664e 100644 --- a/optuna_dashboard/ts/graphUtil.ts +++ b/optuna_dashboard/ts/graphUtil.ts @@ -47,8 +47,8 @@ const getAxisInfoForCategoricalParams = ( a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() - ? 1 - : 0 + ? 1 + : 0 ) return { name: paramName, diff --git a/optuna_dashboard/ts/state.ts b/optuna_dashboard/ts/state.ts index 6d1bc975..8ae05cf2 100644 --- a/optuna_dashboard/ts/state.ts +++ b/optuna_dashboard/ts/state.ts @@ -3,6 +3,7 @@ import { LightColorTemplates, DarkColorTemplates, } from "./components/PlotlyColorTemplates" +import { useQuery } from "./urlQuery" export const studySummariesState = atom({ key: "studySummaries", @@ -37,6 +38,11 @@ export const drawerOpenState = atom({ default: false, }) +export const fetchedTrialsPartiallyState = atom({ + key: "fetchedTrialsPartially", + default: false, +}) + export const isFileUploading = atom({ key: "isFileUploading", default: false, @@ -125,3 +131,18 @@ export const usePlotlyColorTheme = (mode: string): Partial => { return LightColorTemplates[theme.light] } } + +export const useBackendRender = (): boolean => { + const query = useQuery() + const plotlypyIsAvailable = useRecoilValue(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 +} diff --git a/optuna_dashboard/ts/types/index.d.ts b/optuna_dashboard/ts/types/index.d.ts index 30d6708f..c018a077 100644 --- a/optuna_dashboard/ts/types/index.d.ts +++ b/optuna_dashboard/ts/types/index.d.ts @@ -220,6 +220,7 @@ type StudyDetail = { plotly_graph_objects: PlotlyGraphObject[] artifacts: Artifact[] skipped_trial_numbers: number[] + fetched_trials_partially: boolean } type StudyDetails = { diff --git a/package-lock.json b/package-lock.json index 1b432956..f6a8bdc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index cfed3c3b..08bd980d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/python_tests/test_api.py b/python_tests/test_api.py index d644635f..96035255 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -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( diff --git a/python_tests/test_cached_extra_study_property.py b/python_tests/test_cached_extra_study_property.py index dcd5bc5e..381cbcca 100644 --- a/python_tests/test_cached_extra_study_property.py +++ b/python_tests/test_cached_extra_study_property.py @@ -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"}, diff --git a/python_tests/test_serializers.py b/python_tests/test_serializers.py index a991d5d3..c37587c8 100644 --- a/python_tests/test_serializers.py +++ b/python_tests/test_serializers.py @@ -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"] diff --git a/setup.cfg b/setup.cfg index 93d9ad56..1eabbdce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,7 @@ [flake8] +ignore = + E203 + W503 max-line-length = 99 statistics = True exclude = venv,build diff --git a/standalone_app/src/components/DataGrid.tsx b/standalone_app/src/components/DataGrid.tsx index 2855f1f7..ec70e294 100644 --- a/standalone_app/src/components/DataGrid.tsx +++ b/standalone_app/src/components/DataGrid.tsx @@ -65,8 +65,8 @@ function DataGrid(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) => { diff --git a/standalone_app/src/components/PlotHistory.tsx b/standalone_app/src/components/PlotHistory.tsx index 0b421e5a..24f90034 100644 --- a/standalone_app/src/components/PlotHistory.tsx +++ b/standalone_app/src/components/PlotHistory.tsx @@ -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 => { diff --git a/standalone_app/src/sqlite3.ts b/standalone_app/src/sqlite3.ts index d01a7e52..a0bf018b 100644 --- a/standalone_app/src/sqlite3.ts +++ b/standalone_app/src/sqlite3.ts @@ -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], }) }, })