From b1162f4d7d19823ba956830cad88e97a58c2726f Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Mon, 3 Jul 2023 00:10:30 +0900 Subject: [PATCH 1/6] Merge the implementation of GraphHistory and GraphHistoryMultiStudies --- .../ts/components/GraphHistory.tsx | 7 ++-- .../ts/components/StudyHistory.tsx | 30 +++++++------- optuna_dashboard/ts/trialFilter.ts | 41 +++++++++++++++++++ 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphHistory.tsx b/optuna_dashboard/ts/components/GraphHistory.tsx index 0410c3b5..d5e522cf 100644 --- a/optuna_dashboard/ts/components/GraphHistory.tsx +++ b/optuna_dashboard/ts/components/GraphHistory.tsx @@ -20,6 +20,7 @@ import { useFilteredTrialsFromStudies, Target, useObjectiveAndUserAttrTargets, + useObjectiveAndUserAttrTargetsFromStudies, } from "../trialFilter" const plotDomId = "graph-history" @@ -177,10 +178,8 @@ export const GraphHistoryMultiStudies: FC<{ >("number") const [markerSize, setMarkerSize] = useState(5) - // TODO(umezawa): Prepare targets with all studies. - const [targets, selected, setTarget] = useObjectiveAndUserAttrTargets( - studies.length !== 0 ? studies[0] : null - ) + const [targets, selected, setTarget] = + useObjectiveAndUserAttrTargetsFromStudies(studies) const trials = useFilteredTrialsFromStudies( studies, diff --git a/optuna_dashboard/ts/components/StudyHistory.tsx b/optuna_dashboard/ts/components/StudyHistory.tsx index c39f3e95..e988e59a 100644 --- a/optuna_dashboard/ts/components/StudyHistory.tsx +++ b/optuna_dashboard/ts/components/StudyHistory.tsx @@ -9,7 +9,7 @@ import { useTheme, } from "@mui/material" import { GraphParetoFront } from "./GraphParetoFront" -import { GraphHistory } from "./GraphHistory" +import { GraphHistoryMultiStudies } from "./GraphHistory" import { GraphTimeline } from "./GraphTimeline" import { GraphIntermediateValues } from "./GraphIntermediateValues" import Grid2 from "@mui/material/Unstable_Grid2" @@ -89,19 +89,21 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { ) : null} - - - - - + {studyDetail !== null ? ( + + + + + + ) : null} {studyDetail !== null && studyDetail.directions.length == 1 && diff --git a/optuna_dashboard/ts/trialFilter.ts b/optuna_dashboard/ts/trialFilter.ts index 96fb308f..6c829264 100644 --- a/optuna_dashboard/ts/trialFilter.ts +++ b/optuna_dashboard/ts/trialFilter.ts @@ -192,3 +192,44 @@ export const useObjectiveAndUserAttrTargets = ( ) return [targetList, selectedTarget, setTargetIdent] } + +export const useObjectiveAndUserAttrTargetsFromStudies = ( + studies: StudyDetail[] +): [Target[], Target, (ident: string) => void] => { + const defaultTarget = new Target("objective", 0) + const [selected, setTargetIdent] = useState( + defaultTarget.identifier() + ) + const maxDirections = useMemo(() => { + return studies.reduce((acc, study) => { + return Math.max(acc, study.directions.length) + }, 0) + }, [studies]) + + const attrTargets = useMemo(() => { + const uniqueAttrs = Array.from( + new Set(studies.flatMap((study) => study.union_user_attrs)) + ) + return uniqueAttrs.map((attr) => new Target("user_attr", attr.key)) + }, [studies]) + + const targetList = useMemo(() => { + if (studies !== null) { + return [ + ...Array.from( + { length: maxDirections }, + (_, i) => new Target("objective", i) + ), + ...attrTargets, + ] + } else { + return [defaultTarget] + } + }, [maxDirections, attrTargets]) + + const selectedTarget = useMemo( + () => targetList.find((t) => t.identifier() === selected) || defaultTarget, + [targetList, selected] + ) + return [targetList, selectedTarget, setTargetIdent] +} From c04551129a3e190a1d87456a5ad9e1ddb0d7d979 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Sun, 9 Jul 2023 15:39:12 +0900 Subject: [PATCH 2/6] Use intersect of attrs, not union --- optuna_dashboard/ts/trialFilter.ts | 32 +++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/optuna_dashboard/ts/trialFilter.ts b/optuna_dashboard/ts/trialFilter.ts index 6c829264..142ec37f 100644 --- a/optuna_dashboard/ts/trialFilter.ts +++ b/optuna_dashboard/ts/trialFilter.ts @@ -200,24 +200,38 @@ export const useObjectiveAndUserAttrTargetsFromStudies = ( const [selected, setTargetIdent] = useState( defaultTarget.identifier() ) - const maxDirections = useMemo(() => { + const minDirections = useMemo(() => { return studies.reduce((acc, study) => { - return Math.max(acc, study.directions.length) - }, 0) + return Math.min(acc, study.directions.length) + }, Number.MAX_VALUE) }, [studies]) - const attrTargets = useMemo(() => { - const uniqueAttrs = Array.from( - new Set(studies.flatMap((study) => study.union_user_attrs)) + const intersect = (arrays: AttributeSpec[][]) => { + const deepEqual = (obj1: AttributeSpec, obj2: AttributeSpec) => { + return JSON.stringify(obj1) === JSON.stringify(obj2) + } + return arrays.reduce((a, b) => + a.filter((c) => b.some((d) => deepEqual(c, d))) ) - return uniqueAttrs.map((attr) => new Target("user_attr", attr.key)) + } + + const attrTargets = useMemo(() => { + if (studies.length === 0) { + return [] + } + const intersection = intersect( + studies.map((study) => study.union_user_attrs) + ) + return intersection + .filter((attr) => attr.sortable) + .map((attr) => new Target("user_attr", attr.key)) }, [studies]) const targetList = useMemo(() => { if (studies !== null) { return [ ...Array.from( - { length: maxDirections }, + { length: minDirections }, (_, i) => new Target("objective", i) ), ...attrTargets, @@ -225,7 +239,7 @@ export const useObjectiveAndUserAttrTargetsFromStudies = ( } else { return [defaultTarget] } - }, [maxDirections, attrTargets]) + }, [minDirections, attrTargets]) const selectedTarget = useMemo( () => targetList.find((t) => t.identifier() === selected) || defaultTarget, From c790f411752747cfae8c835417e2e44f7533f3f1 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Sun, 9 Jul 2023 15:50:51 +0900 Subject: [PATCH 3/6] Rename GraphHistoryMultiStudies to GraphHistory --- .../ts/components/CompareStudies.tsx | 4 +- .../ts/components/GraphHistory.tsx | 270 ------------------ .../ts/components/StudyHistory.tsx | 4 +- 3 files changed, 4 insertions(+), 274 deletions(-) diff --git a/optuna_dashboard/ts/components/CompareStudies.tsx b/optuna_dashboard/ts/components/CompareStudies.tsx index a60e037e..692c826d 100644 --- a/optuna_dashboard/ts/components/CompareStudies.tsx +++ b/optuna_dashboard/ts/components/CompareStudies.tsx @@ -27,7 +27,7 @@ import { actionCreator } from "../action" import { studySummariesState, studyDetailsState } from "../state" import { AppDrawer } from "./AppDrawer" import { GraphEdfMultiStudies } from "./GraphEdf" -import { GraphHistoryMultiStudies } from "./GraphHistory" +import { GraphHistory } from "./GraphHistory" import { useNavigate, useLocation } from "react-router-dom" const useQuery = (): URLSearchParams => { @@ -313,7 +313,7 @@ const StudiesGraph: FC<{ studies: StudySummary[] }> = ({ studies }) => { }} > - = ({ study, logScale, includePruned }) => { - const theme = useTheme() - const [xAxis, setXAxis] = useState< - "number" | "datetime_start" | "datetime_complete" - >("number") - const [markerSize, setMarkerSize] = useState(5) - - const [targets, selected, setTarget] = useObjectiveAndUserAttrTargets(study) - const trials = useFilteredTrials(study, [selected], !includePruned) - - useEffect(() => { - if (study !== null) { - plotHistory( - trials, - study.directions, - selected, - xAxis, - logScale, - theme.palette.mode, - study?.objective_names, - markerSize - ) - } - }, [ - trials, - study?.directions, - selected, - logScale, - xAxis, - theme.palette.mode, - study?.objective_names, - markerSize, - ]) - - const handleObjectiveChange = (event: SelectChangeEvent) => { - setTarget(event.target.value) - } - - const handleXAxisChange = (e: ChangeEvent) => { - if (e.target.value === "number") { - setXAxis("number") - } else if (e.target.value === "datetime_start") { - setXAxis("datetime_start") - } else if (e.target.value === "datetime_complete") { - setXAxis("datetime_complete") - } - } - - return ( - - - - History - - {targets.length >= 2 ? ( - - y Axis - - - ) : null} - - X-axis: - - } - label="Number" - /> - } - label="Datetime start" - /> - } - label="Datetime complete" - /> - - - - Marker size: - { - // @ts-ignore - setMarkerSize(e.target.value as number) - }} - /> - - - -
- - - ) -} - -export const GraphHistoryMultiStudies: FC<{ studies: StudyDetail[] logScale: boolean includePruned: boolean @@ -305,141 +170,6 @@ export const GraphHistoryMultiStudies: FC<{ } const plotHistory = ( - trials: Trial[], - directions: StudyDirection[], - target: Target, - xAxis: "number" | "datetime_start" | "datetime_complete", - logScale: boolean, - mode: string, - objectiveNames?: string[], - markerSize: number -) => { - if (document.getElementById(plotDomId) === null) { - return - } - - const layout: Partial = { - margin: { - l: 50, - t: 0, - r: 50, - b: 0, - }, - yaxis: { - title: target.toLabel(objectiveNames), - type: logScale ? "log" : "linear", - }, - xaxis: { - title: xAxis === "number" ? "Trial" : "Time", - type: xAxis === "number" ? "linear" : "date", - }, - showlegend: true, - uirevision: "true", - template: mode === "dark" ? plotlyDarkTemplate : {}, - } - if (trials.length === 0) { - plotly.react(plotDomId, [], layout) - return - } - - const feasibleTrials: Trial[] = [] - const infeasibleTrials: Trial[] = [] - trials.forEach((t) => { - if (t.constraints.every((c) => c <= 0)) { - feasibleTrials.push(t) - } else { - infeasibleTrials.push(t) - } - }) - - const getAxisX = (trial: Trial): number | Date => { - return xAxis === "number" - ? trial.number - : xAxis === "datetime_start" - ? trial.datetime_start! - : trial.datetime_complete! - } - - const plotData: Partial[] = [ - { - x: feasibleTrials.map(getAxisX), - y: feasibleTrials.map( - (t: Trial): number => target.getTargetValue(t) as number - ), - name: target.toLabel(objectiveNames), - marker: { - size: markerSize, - }, - mode: "markers", - type: "scatter", - }, - ] - - const objectiveId = target.getObjectiveId() - if (objectiveId !== null) { - const xForLinePlot: (number | Date)[] = [] - const yForLinePlot: number[] = [] - let currentBest: number | null = null - for (let i = 0; i < feasibleTrials.length; i++) { - const t = feasibleTrials[i] - if (currentBest === null) { - currentBest = t.values![objectiveId] as number - xForLinePlot.push(getAxisX(t)) - yForLinePlot.push(t.values![objectiveId] as number) - } else if ( - directions[objectiveId] === "maximize" && - t.values![objectiveId] > currentBest - ) { - const p = trials[i - 1] - if (!xForLinePlot.includes(getAxisX(p))) { - xForLinePlot.push(getAxisX(p)) - yForLinePlot.push(currentBest) - } - currentBest = t.values![objectiveId] as number - xForLinePlot.push(getAxisX(t)) - yForLinePlot.push(t.values![objectiveId] as number) - } else if ( - directions[objectiveId] === "minimize" && - t.values![objectiveId] < currentBest - ) { - const p = feasibleTrials[i - 1] - if (!xForLinePlot.includes(getAxisX(p))) { - xForLinePlot.push(getAxisX(p)) - yForLinePlot.push(currentBest) - } - currentBest = t.values![objectiveId] as number - xForLinePlot.push(getAxisX(t)) - yForLinePlot.push(t.values![objectiveId] as number) - } - } - xForLinePlot.push(getAxisX(trials[trials.length - 1])) - yForLinePlot.push(yForLinePlot[yForLinePlot.length - 1]) - plotData.push({ - x: xForLinePlot, - y: yForLinePlot, - name: "Best Value", - mode: "lines", - type: "scatter", - }) - } - plotData.push({ - x: infeasibleTrials.map(getAxisX), - y: infeasibleTrials.map( - (t: Trial): number => target.getTargetValue(t) as number - ), - name: "Infeasible Trial", - marker: { - size: markerSize, - color: mode === "dark" ? "#666666" : "#cccccc", - }, - mode: "markers", - type: "scatter", - showlegend: false, - }) - plotly.react(plotDomId, plotData, layout) -} - -const plotHistoryMultiStudies = ( historyPlotInfos: HistoryPlotInfo[], target: Target, xAxis: "number" | "datetime_start" | "datetime_complete", diff --git a/optuna_dashboard/ts/components/StudyHistory.tsx b/optuna_dashboard/ts/components/StudyHistory.tsx index e988e59a..dc5f1289 100644 --- a/optuna_dashboard/ts/components/StudyHistory.tsx +++ b/optuna_dashboard/ts/components/StudyHistory.tsx @@ -9,7 +9,7 @@ import { useTheme, } from "@mui/material" import { GraphParetoFront } from "./GraphParetoFront" -import { GraphHistoryMultiStudies } from "./GraphHistory" +import { GraphHistory } from "./GraphHistory" import { GraphTimeline } from "./GraphTimeline" import { GraphIntermediateValues } from "./GraphIntermediateValues" import Grid2 from "@mui/material/Unstable_Grid2" @@ -96,7 +96,7 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { }} > - Date: Tue, 18 Jul 2023 16:30:52 +0900 Subject: [PATCH 4/6] Fix typo --- optuna_dashboard/ts/components/GraphHistory.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphHistory.tsx b/optuna_dashboard/ts/components/GraphHistory.tsx index 5f428385..73cf2f09 100644 --- a/optuna_dashboard/ts/components/GraphHistory.tsx +++ b/optuna_dashboard/ts/components/GraphHistory.tsx @@ -62,7 +62,7 @@ export const GraphHistory: FC<{ }) useEffect(() => { - plotHistoryMultiStudies( + plotHistory( historyPlotInfos, selected, xAxis, @@ -70,7 +70,7 @@ export const GraphHistory: FC<{ theme.palette.mode, markerSize ) - }, [studies, selected, logScale, xAxis, theme.palette.mode]) + }, [studies, selected, logScale, xAxis, theme.palette.mode, markerSize]) const handleObjectiveChange = (event: SelectChangeEvent) => { setTarget(event.target.value) From 025e061d06bcba706fd75b79a20709f4693718df Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Sat, 29 Jul 2023 16:53:14 +0900 Subject: [PATCH 5/6] Fix CLS issue --- .../ts/components/GraphHistory.tsx | 8 +++++- .../ts/components/StudyHistory.tsx | 28 +++++++++---------- optuna_dashboard/ts/trialFilter.ts | 3 ++ 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphHistory.tsx b/optuna_dashboard/ts/components/GraphHistory.tsx index 73cf2f09..3093d82b 100644 --- a/optuna_dashboard/ts/components/GraphHistory.tsx +++ b/optuna_dashboard/ts/components/GraphHistory.tsx @@ -1,6 +1,7 @@ import * as plotly from "plotly.js-dist-min" import React, { ChangeEvent, FC, useEffect, useState } from "react" import { + Box, Grid, FormControl, FormLabel, @@ -163,7 +164,12 @@ export const GraphHistory: FC<{ -
+ ) diff --git a/optuna_dashboard/ts/components/StudyHistory.tsx b/optuna_dashboard/ts/components/StudyHistory.tsx index dc5f1289..ce30fc44 100644 --- a/optuna_dashboard/ts/components/StudyHistory.tsx +++ b/optuna_dashboard/ts/components/StudyHistory.tsx @@ -89,21 +89,19 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { ) : null} - {studyDetail !== null ? ( - - - - - - ) : null} + + + + + {studyDetail !== null && studyDetail.directions.length == 1 && diff --git a/optuna_dashboard/ts/trialFilter.ts b/optuna_dashboard/ts/trialFilter.ts index 142ec37f..81e82fda 100644 --- a/optuna_dashboard/ts/trialFilter.ts +++ b/optuna_dashboard/ts/trialFilter.ts @@ -201,6 +201,9 @@ export const useObjectiveAndUserAttrTargetsFromStudies = ( defaultTarget.identifier() ) const minDirections = useMemo(() => { + if (studies.length === 0) { + return 0 + } return studies.reduce((acc, study) => { return Math.min(acc, study.directions.length) }, Number.MAX_VALUE) From 3a5ed27d6adf159721b7b11cc796a3aff75e0290 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Sat, 29 Jul 2023 16:57:34 +0900 Subject: [PATCH 6/6] Fix equality of attribute --- optuna_dashboard/ts/trialFilter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/optuna_dashboard/ts/trialFilter.ts b/optuna_dashboard/ts/trialFilter.ts index 81e82fda..4fe7e35a 100644 --- a/optuna_dashboard/ts/trialFilter.ts +++ b/optuna_dashboard/ts/trialFilter.ts @@ -210,11 +210,11 @@ export const useObjectiveAndUserAttrTargetsFromStudies = ( }, [studies]) const intersect = (arrays: AttributeSpec[][]) => { - const deepEqual = (obj1: AttributeSpec, obj2: AttributeSpec) => { - return JSON.stringify(obj1) === JSON.stringify(obj2) + const atrEqual = (obj1: AttributeSpec, obj2: AttributeSpec) => { + return obj1.key === obj2.key } return arrays.reduce((a, b) => - a.filter((c) => b.some((d) => deepEqual(c, d))) + a.filter((c) => b.some((d) => atrEqual(c, d))) ) }