This commit is contained in:
moririn2528
2023-09-06 15:51:25 +09:00
parent 436afe7dc2
commit b044ec9b68
11 changed files with 1698 additions and 65 deletions
+2
View File
@@ -18,6 +18,7 @@ from ._named_objectives import get_objective_names
from ._preferential_history import _SYSTEM_ATTR_PREFIX_HISTORY
from .artifact._backend import list_trial_artifacts
from .preferential._study import _SYSTEM_ATTR_PREFERENTIAL_STUDY
from .preferential._system_attrs import _get_preferences
if TYPE_CHECKING:
@@ -162,6 +163,7 @@ def serialize_study_detail(
serialized["form_widgets"] = form_widgets
if serialized["is_preferential"]:
serialized["preference_history"] = serialize_preference_history(system_attrs)
serialized["preferences"] = _get_preferences(system_attrs)
return serialized
@@ -35,12 +35,8 @@ def report_preferences(
return preference_id
def get_preferences(
study_id: int,
storage: BaseStorage,
) -> list[tuple[int, int]]:
def _get_preferences(system_attrs: dict[str, Any]) -> list[tuple[int, int]]:
preferences: list[tuple[int, int]] = []
system_attrs = storage.get_study_system_attrs(study_id)
for k, v in system_attrs.items():
if not k.startswith(_SYSTEM_ATTR_PREFIX_PREFERENCE):
continue
@@ -48,6 +44,13 @@ def get_preferences(
return preferences
def get_preferences(
study_id: int,
storage: BaseStorage,
) -> list[tuple[int, int]]:
return _get_preferences(storage.get_study_system_attrs(study_id))
def report_skip(
study_id: int,
trial_id: int,
+2
View File
@@ -92,6 +92,7 @@ interface StudyDetailResponse {
is_preferential: boolean
objective_names?: string[]
form_widgets?: FormWidgets
preferences?: [number, number][]
preference_history?: PreferenceHistoryResponce[]
}
@@ -128,6 +129,7 @@ export const getStudyDetailAPI = (
objective_names: res.data.objective_names,
form_widgets: res.data.form_widgets,
is_preferential: res.data.is_preferential,
preferences: res.data.preferences,
preference_history: res.data.preference_history?.map(
convertPreferenceHistory
),
+9
View File
@@ -87,6 +87,15 @@ export const App: FC = () => {
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/graph"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"graph"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId"}
element={
@@ -35,6 +35,7 @@ import OpenInNewIcon from "@mui/icons-material/OpenInNew"
import QueryStatsIcon from "@mui/icons-material/QueryStats"
import ThumbUpAltIcon from "@mui/icons-material/ThumbUpAlt"
import HistoryIcon from "@mui/icons-material/History"
import LanIcon from "@mui/icons-material/Lan"
import { Switch } from "@mui/material"
import { actionCreator } from "../action"
@@ -47,6 +48,7 @@ export type PageId =
| "trialList"
| "note"
| "preferenceHistory"
| "graph"
const openedMixin = (theme: Theme): CSSObject => ({
width: drawerWidth,
@@ -246,6 +248,21 @@ export const AppDrawer: FC<{
<ListItemText primary="Analytics" sx={styleListItemText} />
</ListItemButton>
</ListItem>
{studyDetail?.is_preferential && (
<ListItem key="Graph" disablePadding sx={styleListItem}>
<ListItemButton
component={Link}
to={`${URL_PREFIX}/studies/${studyId}/graph`}
sx={styleListItemButton}
selected={page === "graph"}
>
<ListItemIcon sx={styleListItemIcon}>
<LanIcon />
</ListItemIcon>
<ListItemText primary="Graph" sx={styleListItemText} />
</ListItemButton>
</ListItem>
)}
<ListItem key="TableList" disablePadding sx={styleListItem}>
<ListItemButton
component={Link}
@@ -0,0 +1,156 @@
import React, { FC, useState, useCallback, useMemo, useEffect } from "react"
import {
Box,
Card,
CardContent,
CardHeader,
Paper,
Typography,
useTheme,
} from "@mui/material"
import Grid2 from "@mui/material/Unstable_Grid2"
import { DataGrid, DataGridColumn } from "./DataGrid"
import { BestTrialsCard } from "./BestTrialsCard"
import { useStudyDetailValue, useStudySummaryValue } from "../state"
import { Contour } from "./GraphContour"
import { MarkdownRenderer } from "./Note"
import ReactFlow, {
addEdge,
Node,
NodeProps,
NodeTypes,
Edge,
FitViewOptions,
DefaultEdgeOptions,
applyNodeChanges,
applyEdgeChanges,
OnNodesChange,
OnEdgesChange,
OnConnect,
Position,
Handle,
} from "reactflow"
import "reactflow/dist/style.css"
const nodeWidth = 400
const nodeHeight = 300
type NodeData = {
trial?: Trial
}
const GraphNode: FC<NodeProps<NodeData>> = ({ data, isConnectable }) => {
const theme = useTheme()
const trial = data.trial
if (trial === undefined) {
return null
}
const noteBody = trial.note.body
const noteFC = useMemo(() => {
return <MarkdownRenderer body={noteBody} />
}, [noteBody])
return (
<Card
sx={{
width: nodeWidth,
height: nodeHeight,
overflow: "hidden",
}}
>
<CardHeader title={`Trial ${trial.number}`} />
<Handle
type="target"
position={Position.Top}
style={{ background: "#555" }}
isConnectable={isConnectable}
/>
<CardContent>{noteFC}</CardContent>
<Handle
type="source"
position={Position.Bottom}
style={{ background: "#555" }}
isConnectable={isConnectable}
/>
</Card>
)
}
const nodeTypes: NodeTypes = {
note: GraphNode,
}
const createNode = (x: number, y: number, trial: Trial): Node => {
return {
id: `${trial.number}`,
type: "note",
data: {
label: `Trial ${trial.number}`,
trial: trial,
},
position: {
x: x * 500,
y: y * 400,
},
style: {
width: nodeWidth,
height: nodeHeight,
padding: 0,
},
}
}
const defaultEdgeOptions: DefaultEdgeOptions = {
animated: true,
}
export const PreferentialGraph: FC<{ studyDetail: StudyDetail | null }> = ({
studyDetail,
}) => {
if (studyDetail === null || !studyDetail.is_preferential) {
return null
}
const [nodes, setNodes] = useState<Node[]>([])
const onNodesChange: OnNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes]
)
useEffect(() => {
setNodes((prev) => {
const newNodes: Node[] = []
studyDetail.best_trials.forEach((trial, i) => {
newNodes.push(createNode(i, 0, trial))
})
if (studyDetail.preference_history !== undefined) {
const histories = [...studyDetail.preference_history]
histories?.reverse().forEach((history, i) => {
const y = history.candidates.findIndex((c) => c === history.clicked)
newNodes.push(
createNode(y, i + 1, studyDetail.trials[history.clicked])
)
})
}
return newNodes
})
}, [studyDetail])
const edges: Edge[] =
studyDetail.preferences?.map((p) => {
return {
id: `e${p[0]}-${p[1]}`,
source: `${p[0]}`,
target: `${p[1]}`,
style: { stroke: "#fff" },
} as Edge
}) ?? []
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
defaultEdgeOptions={defaultEdgeOptions}
nodeTypes={nodeTypes}
zoomOnScroll={false}
panOnScroll={true}
/>
)
}
@@ -32,6 +32,7 @@ import { StudyHistory } from "./StudyHistory"
import { PreferentialTrials } from "./PreferentialTrials"
import { PreferenceHistory } from "./PreferenceHistory"
import { PreferentialAnalytics } from "./PreferentialAnalytics"
import { PreferentialGraph } from "./PreferentialGraph"
interface ParamTypes {
studyId: string
@@ -176,6 +177,17 @@ export const StudyDetail: FC<{
/>
</Box>
)
} else if (page === "graph") {
content = (
<Box
sx={{
height: `calc(100vh - ${theme.spacing(8)})`,
padding: theme.spacing(2),
}}
>
<PreferentialGraph studyDetail={studyDetail} />
</Box>
)
} else if (page == "preferenceHistory") {
content = <PreferenceHistory studyDetail={studyDetail} />
}
+3 -1
View File
@@ -198,6 +198,7 @@ type StudyDetail = {
is_preferential: boolean
objective_names?: string[]
form_widgets?: FormWidgets
preferences?: [number, number][]
preference_history?: PreferenceHistory[]
}
@@ -208,7 +209,6 @@ type StudyDetails = {
type StudyParamImportance = {
[study_id: string]: ParamImportance[][]
}
type PreferenceHistory = {
id: string
preference_id: string
@@ -217,3 +217,5 @@ type PreferenceHistory = {
feedback_mode: PreferenceFeedbackMode
timestamp: Date
}
declare module "*.css"
+1411
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -34,6 +34,7 @@
"react-markdown": "^8.0.4",
"react-router-dom": "^6.11.0",
"react-syntax-highlighter": "^15.5.0",
"reactflow": "^11.8.3",
"recoil": "^0.7.7",
"rehype-mathjax": "^4.0.2",
"rehype-raw": "^6.1.1",
@@ -53,12 +54,14 @@
"@typescript-eslint/eslint-plugin": "^4.26.1",
"@typescript-eslint/parser": "^4.26.1",
"compression-webpack-plugin": "^10.0.0",
"css-loader": "^6.8.1",
"esbuild-loader": "^2.18.0",
"eslint": "^7.28.0",
"jest": "^29.2.1",
"jest-canvas-mock": "^2.3.1",
"jest-environment-jsdom": "^29.3.1",
"prettier": "^2.5.1",
"style-loader": "^3.3.3",
"ts-jest": "^29.0.3",
"ts-loader": "^9.2.7",
"typescript": "^4.6.2",
+75 -59
View File
@@ -1,65 +1,81 @@
const webpack = require('webpack');
const webpack = require("webpack")
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
const isDev = mode === 'development';
const mode =
process.env.NODE_ENV === "production" ? "production" : "development"
const isDev = mode === "development"
const typeScriptLoader = process.env.TYPESCRIPT_LOADER === "esbuild-loader" ? {
test: /\.tsx?$/,
exclude: [/node_modules/],
loader: 'esbuild-loader',
options: {
loader: 'tsx',
tsconfigRaw: require('./tsconfig.json')
}
} : {
test: /\.tsx?$/,
exclude: [/node_modules/],
loader: 'ts-loader',
options: {
configFile: __dirname + '/tsconfig.json',
transpileOnly: isDev,
happyPackMode: true
}
}
const typeScriptLoader =
process.env.TYPESCRIPT_LOADER === "esbuild-loader"
? {
test: /\.tsx?$/,
exclude: [/node_modules/],
loader: "esbuild-loader",
options: {
loader: "tsx",
tsconfigRaw: require("./tsconfig.json"),
},
}
: {
test: /\.tsx?$/,
exclude: [/node_modules/],
loader: "ts-loader",
options: {
configFile: __dirname + "/tsconfig.json",
transpileOnly: isDev,
happyPackMode: true,
},
}
var config = {
mode,
entry: [__dirname + '/optuna_dashboard/ts/index.tsx'],
output: {
path: __dirname + '/optuna_dashboard/public/',
filename: 'bundle.js',
publicPath: '/public/'
},
module: {
rules: [{oneOf: [typeScriptLoader]}]
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
plugins: [
new webpack.DefinePlugin({
'APP_BAR_TITLE': JSON.stringify(process.env.APP_BAR_TITLE || "Optuna Dashboard"),
'API_ENDPOINT': JSON.stringify(process.env.API_ENDPOINT),
'URL_PREFIX': JSON.stringify(process.env.URL_PREFIX || "/dashboard")
})
]
};
if (isDev) {
config.devtool = 'source-map';
config.cache = {
type: 'filesystem',
buildDependencies: {
config: [__filename],
}
}
console.log('= = = = = = = = = = = = = = = = = = =');
console.log('DEVELOPMENT BUILD');
console.log(process.env.TYPESCRIPT_LOADER === 'esbuild-loader' ? 'esbuild-loader' : 'ts-loader');
console.log('= = = = = = = = = = = = = = = = = = =');
} else {
const CompressionPlugin = require("compression-webpack-plugin");
config.plugins.push(new CompressionPlugin())
mode,
entry: [__dirname + "/optuna_dashboard/ts/index.tsx"],
output: {
path: __dirname + "/optuna_dashboard/public/",
filename: "bundle.js",
publicPath: "/public/",
},
module: {
rules: [
{ oneOf: [typeScriptLoader] },
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
plugins: [
new webpack.DefinePlugin({
APP_BAR_TITLE: JSON.stringify(
process.env.APP_BAR_TITLE || "Optuna Dashboard"
),
API_ENDPOINT: JSON.stringify(process.env.API_ENDPOINT),
URL_PREFIX: JSON.stringify(process.env.URL_PREFIX || "/dashboard"),
}),
],
}
module.exports = config;
if (isDev) {
config.devtool = "source-map"
config.cache = {
type: "filesystem",
buildDependencies: {
config: [__filename],
},
}
console.log("= = = = = = = = = = = = = = = = = = =")
console.log("DEVELOPMENT BUILD")
console.log(
process.env.TYPESCRIPT_LOADER === "esbuild-loader"
? "esbuild-loader"
: "ts-loader"
)
console.log("= = = = = = = = = = = = = = = = = = =")
} else {
const CompressionPlugin = require("compression-webpack-plugin")
config.plugins.push(new CompressionPlugin())
}
module.exports = config