Merge branch 'main' into enhance/speedup-get-trials

This commit is contained in:
keisuke umezawa
2024-02-06 21:41:48 +09:00
committed by GitHub
68 changed files with 9473 additions and 8428 deletions
+2 -7
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
optuna-version: ['optuna==2.10.0', 'git+https://github.com/optuna/optuna.git']
optuna-version: ['optuna==3.1.0', 'git+https://github.com/optuna/optuna.git']
steps:
- uses: actions/checkout@v2
@@ -48,9 +48,4 @@ jobs:
- name: Run e2e tests
run: |
if [ "${{ matrix.optuna-version }}" = "optuna==2.10.0" ]; then
ignore_option="--ignore e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py"
else
ignore_option=""
fi
pytest e2e_tests/test_dashboard $ignore_option
pytest e2e_tests/test_dashboard || true
-1
View File
@@ -28,7 +28,6 @@ jobs:
python -m pip install --progress-bar off --upgrade pip setuptools
pip install --progress-bar off .[optional]
pip install --progress-bar off .[test]
pip install --progress-bar off "optuna>=3.0.0"
pip install --progress-bar off .
echo 'import coverage; coverage.process_startup()' > sitecustomize.py
- name: Tests
+31 -10
View File
@@ -18,10 +18,8 @@ jobs:
python-version: '3.10'
architecture: x64
- name: Install dependencies
# python_tests requires optuna>=3.0.0 since it imports FloatDistribution
run: |
python -m pip install --progress-bar off --upgrade pip setuptools
pip install --progress-bar off "optuna>=3.0.0"
pip install --progress-bar off .
pip install --progress-bar off flake8 black isort mypy mypy-boto3-s3
- run: flake8 . --show-source
@@ -32,9 +30,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
# Add Python '3.11' after released Optuna v3.1
# since Optuna v3.0.4 depends on scipy<1.9.0 and >=1.7.0
python-version: ['3.7', '3.8', '3.9', '3.10']
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
optuna-version: ['optuna>=3.1.0']
steps:
- uses: actions/checkout@v2
- name: Setup Python${{ matrix.python-version }}
@@ -42,15 +39,18 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
architecture: x64
- name: Install dependencies
# python_tests requires optuna>=3.0.0 since it imports FloatDistribution
- name: Setup Optuna ${{ matrix.optuna-version }}
run: |
python -m pip install --progress-bar off --upgrade pip setuptools
python -m pip install --progress-bar off --upgrade ${{ matrix.optuna-version }}
- name: Install dependencies
run: |
pip install --progress-bar off .[optional]
pip install --progress-bar off .[test]
pip install --progress-bar off "optuna>=3.0.0"
pip install --progress-bar off .
- run: pytest python_tests
- name: Run python unit tests
run: |
pytest python_tests
test-with-optuna-master:
runs-on: ubuntu-latest
steps:
@@ -67,4 +67,25 @@ jobs:
pip install --progress-bar off .[test]
pip install --progress-bar off .
python -m pip install --progress-bar off --upgrade git+https://github.com/optuna/optuna.git
- run: pytest python_tests
- name: Run python unit tests
run: |
pytest python_tests
test-with-optuna-oldest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
architecture: x64
- name: Install dependencies
run: |
python -m pip install --progress-bar off --upgrade pip setuptools
pip install --progress-bar off .[optional]
pip install --progress-bar off .[test]
pip install --progress-bar off .
python -m pip install --progress-bar off --force-reinstall optuna==3.1.0
- name: Run python unit tests
run: |
pytest python_tests
-2
View File
@@ -1,2 +0,0 @@
optuna_dashboard/ts/components/PlotlyDarkMode.ts
standalone_app/src/PlotlyDarkMode.ts
-4
View File
@@ -1,4 +0,0 @@
trailingComma: "es5"
tabWidth: 2
semi: false
singleQuote: false
+4 -4
View File
@@ -1,4 +1,4 @@
FROM node:16 AS front-builder
FROM node:20 AS front-builder
WORKDIR /usr/src
ADD ./package.json /usr/src/package.json
@@ -11,7 +11,7 @@ ADD ./optuna_dashboard/ts/ /usr/src/optuna_dashboard/ts
RUN mkdir -p /usr/src/optuna_dashboard/public
RUN npm run build:prd
FROM python:3.10-buster AS python-builder
FROM python:3.11-buster AS python-builder
WORKDIR /usr/src
RUN pip install --upgrade pip setuptools
@@ -23,9 +23,9 @@ ADD ./optuna_dashboard /usr/src/optuna_dashboard
COPY --from=front-builder /usr/src/optuna_dashboard/public/ /usr/src/optuna_dashboard/public/
RUN pip install --progress-bar off .
FROM python:3.10-slim-buster as runner
FROM python:3.11-slim-buster as runner
COPY --from=python-builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
COPY --from=python-builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=python-builder /usr/local/bin/optuna-dashboard /usr/local/bin/optuna-dashboard
RUN mkdir /app
+28
View File
@@ -0,0 +1,28 @@
{
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"files": {
"include": [
"optuna_dashboard/ts/**/*.ts",
"optuna_dashboard/ts/**/*.tsx",
"typescript_tests/**/*.ts",
"typescript_tests/**/*.tsx",
"standalone_app/src/**/*.ts",
"standalone_app/src/**/*.tsx",
"vscode/src/**/*.ts",
"vscode/src/**/*.tsx"
],
"ignore": [
"optuna_dashboard/ts/components/PlotlyDarkMode.ts",
"standalone_app/src/PlotlyDarkMode.ts"
]
},
"javascript": {
"formatter": {
"trailingComma": "es5",
"indentWidth": 2,
"indentStyle": "space",
"semicolons": "asNeeded",
"quoteStyle": "double"
}
}
}
+1 -16
View File
@@ -12,9 +12,9 @@ General APIs
optuna_dashboard.run_server
optuna_dashboard.wsgi
optuna_dashboard.set_objective_names
optuna_dashboard.save_note
optuna_dashboard.save_plotly_graph_object
optuna_dashboard.artifact.get_artifact_path
Human-in-the-loop
-----------------
@@ -57,18 +57,3 @@ Streamlit
optuna_dashboard.streamlit.render_trial_note
optuna_dashboard.streamlit.render_objective_form_widgets
optuna_dashboard.streamlit.render_user_attr_form_widgets
Artifact
--------
.. autosummary::
:toctree: _generated/
:nosignatures:
optuna_dashboard.artifact.upload_artifact
optuna_dashboard.artifact.file_system.FileSystemBackend
optuna_dashboard.artifact.boto3.Boto3Backend
optuna_dashboard.artifact.backoff.Backoff
optuna_dashboard.artifact.prefix.AppendPrefix
optuna_dashboard.artifact.protocol.ArtifactBackend
optuna_dashboard.artifact.exceptions.ArtifactNotFound
+4 -4
View File
@@ -27,7 +27,7 @@ or use a following dirty hack to unwrap ``_CachedStorage`` class.
``set_objective_names()`` function is deprecated. Please use ``study.set_metric_names()`` instead.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:func:`~optuna_dashboard.set_objective_names` function has been ported to Optuna.
``set_objective_names`` function has been ported to Optuna.
Please use `study.set_metric_names() <https://optuna.readthedocs.io/en/latest/reference/generated/optuna.study.Study.html#optuna.study.Study>`_ function instead.
.. list-table::
@@ -41,7 +41,7 @@ Please use `study.set_metric_names() <https://optuna.readthedocs.io/en/latest/re
``upload_artifact()`` is deprecated. Please use ``optuna.artifacts.upload_artifact()`` instead.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:func:`~optuna_dashboard.artifact.upload_artifact` function has been ported to Optuna.
``upload_artifact`` function has been ported to Optuna.
Please use `optuna.artifacts.upload_artifact <https://optuna.readthedocs.io/en/latest/reference/generated/optuna.artifacts.upload_artifact.html>`_ function instead.
.. list-table::
@@ -57,7 +57,7 @@ Please note that the order of arguments is different between the deprecated and
``FileSystemBackend`` is deprecated. Please use ``FileSystemArtifactStore`` instead.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:class:`~optuna_dashboard.artifact.file_system.FileSystemBackend` class has been ported to Optuna.
``FileSystemBackend`` class has been ported to Optuna.
Please use `FileSystemArtifactStore <https://optuna.readthedocs.io/en/latest/reference/generated/optuna.artifacts.FileSystemArtifactStore.html>`_ class instead.
.. list-table::
@@ -71,7 +71,7 @@ Please use `FileSystemArtifactStore <https://optuna.readthedocs.io/en/latest/ref
``Boto3Backend``` is deprecated. Please use ``Boto3ArtifactStore`` instead.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:class:`~optuna_dashboard.artifact.boto3.Boto3Backend` class has been ported to Optuna.
``Boto3Backend`` class has been ported to Optuna.
Please use `Boto3ArtifactStore <https://optuna.readthedocs.io/en/latest/reference/generated/optuna.artifacts.Boto3ArtifactStore.html>`_ class instead.
.. list-table::
-44
View File
@@ -1,44 +0,0 @@
#!/bin/sh
DIR=$(cd $(dirname $0); pwd)
OPTUNA_DASHBOARD_IMAGE=${OPTUNA_DASHBOARD_IMAGE:-cbata/optuna-dashboard}
echo ""
echo "1. Prepare MYSQL 8.0 Server using Docker."
echo ""
set -e
docker run \
-d \
--rm \
-p 3306:3306 \
-e MYSQL_USER=optuna \
-e MYSQL_DATABASE=optuna \
-e MYSQL_PASSWORD=password \
-e MYSQL_ALLOW_EMPTY_PASSWORD=yes \
--name optuna-mysql \
mysql:8
echo "Wait ready for MySQL"
sleep 20
echo ""
echo "2. Run an Optuna example."
echo ""
python ${DIR}/optuna_example.py "mysql+pymysql://optuna:password@127.0.0.1:3306/optuna"
set +e
echo ""
echo "3. Open Web Dashboard"
echo ""
docker run -it --rm -p 8080:8080 --link=optuna-mysql $OPTUNA_DASHBOARD_IMAGE "mysql+pymysql://optuna:password@optuna-mysql:3306/optuna"
echo ""
echo "4. Stop MYSQL Server"
echo ""
docker stop optuna-mysql
-43
View File
@@ -1,43 +0,0 @@
#!/bin/sh
DIR=$(cd $(dirname $0); pwd)
OPTUNA_DASHBOARD_IMAGE=${OPTUNA_DASHBOARD_IMAGE:-cbata/optuna-dashboard}
echo ""
echo "1. Run PostgreSQL Server using Docker."
echo ""
set -e
docker run \
-d \
--rm \
-p 5432:5432 \
-e POSTGRES_USER=optuna \
-e POSTGRES_DB=optuna \
-e POSTGRES_PASSWORD=password \
--name optuna-postgres \
postgres:10.1-alpine
echo "Wait ready for PostgreSQL"
sleep 20
echo ""
echo "2. Run an Optuna example."
echo ""
python ${DIR}/optuna_example.py "postgresql+psycopg2://optuna:password@127.0.0.1:5432/optuna"
set +e
echo ""
echo "3. Open Web Dashboard"
echo ""
docker run -it --rm -p 8080:8080 --link=optuna-postgres $OPTUNA_DASHBOARD_IMAGE "postgresql+psycopg2://optuna:password@optuna-postgres:5432/optuna"
echo ""
echo "4. Stop MYSQL Server"
echo ""
docker stop optuna-postgres
-19
View File
@@ -1,19 +0,0 @@
import sys
import optuna
def objective(trial):
x = trial.suggest_float("x", -100, 100)
y = trial.suggest_categorical("y", [-1, 0, 1])
return x**2 + y
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Invalid args")
sys.exit(1)
db_url = sys.argv[1]
study = optuna.create_study(storage=db_url, study_name="example-study")
study.optimize(objective, n_trials=10)
+44 -2
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import csv
import functools
import importlib
import io
from itertools import chain
import logging
@@ -95,6 +96,7 @@ def create_app(
def api_meta() -> dict[str, Any]:
return {
"artifact_is_available": artifact_store is not None,
"plotlypy_is_available": importlib.util.find_spec("plotly") is not None,
}
@app.get("/api/studies")
@@ -270,6 +272,46 @@ def create_app(
response.status = 400 # Bad request
return {"reason": str(e)}
@app.get("/api/studies/<study_id:int>/plot/<plot_type>")
@json_api_view
def get_plot(study_id: int, plot_type: str) -> dict[str, Any]:
study = optuna.load_study(
study_name=storage.get_study_name_from_id(study_id), storage=storage
)
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."}
return fig.to_json()
@app.put("/api/studies/<study_id:int>/note")
@json_api_view
def save_study_note(study_id: int) -> dict[str, Any]:
@@ -477,9 +519,9 @@ def create_app(
param_names_header = [f"Param {x}" for x in param_names]
user_attr_names_header = [f"UserAttribute {x}" for x in user_attr_names]
n_objs = len(study.directions)
if study.metric_names is not None:
if hasattr(study, "metric_names") and study.metric_names is not None:
value_header = study.metric_names
else:
else: # optuna < v3.4.0
value_header = ["Value"] if n_objs == 1 else [f"Objective {x}" for x in range(n_objs)]
column_names = (
["Number", "State"] + value_header + param_names_header + user_attr_names_header
+1 -1
View File
@@ -248,7 +248,7 @@ def serialize_frozen_trial(
],
"user_attrs": serialize_attrs(trial.user_attrs),
"note": note.get_note_from_system_attrs(study_system_attrs, trial._trial_id),
"artifacts": list_trial_artifacts(study_system_attrs, trial),
"artifacts": list_trial_artifacts(study_system_attrs, trial_system_attrs, trial),
"constraints": trial_system_attrs.get(CONSTRAINTS_KEY, []),
}
+23 -6
View File
@@ -52,7 +52,18 @@ def get_artifact_path(
study_or_trial: optuna.Trial | optuna.Study,
artifact_id: str,
) -> str:
"""Get the URL path for a given artifact ID."""
"""Get the URL path for a given artifact ID.
Args:
study_or_trial:
A Trial object, or a Study object.
artifact_id:
An artifact ID.
Returns:
A URL path to the artifact.
"""
if isinstance(study_or_trial, optuna.Study):
study_id = study_or_trial._study_id
return f"/artifacts/{study_id}/{artifact_id}"
@@ -139,10 +150,12 @@ def register_artifact_route(
storage.set_trial_system_attr(trial_id, attr_key, json.dumps(artifact))
response.status = 201
trial = storage.get_trial(trial_id) # Fetch trial.system_attrs again.
study_system_attrs = storage.get_study_system_attrs(study_id)
trial_system_attrs = storage.get_trial_system_attrs(trial_id)
artifacts = list_trial_artifacts(study_system_attrs, trial_system_attrs, trial)
return {
"artifact_id": artifact_id,
"artifacts": list_trial_artifacts(storage.get_study_system_attrs(study_id), trial),
"artifacts": artifacts,
}
@app.post("/api/artifacts/<study_id:int>")
@@ -322,7 +335,11 @@ def delete_all_artifacts(backend: ArtifactStore, storage: BaseStorage, study_id:
study_system_attrs = storage.get_study_system_attrs(study_id)
artifact_metas.extend(list_study_artifacts(study_system_attrs))
for trial in storage.get_all_trials(study_id):
trial_artifacts = list_trial_artifacts(study_system_attrs, trial)
trial_system_attrs = getattr(trial, "_system_attrs")
if trial_system_attrs is None:
# This is unreachable line until Optuna v5.0.0 release.
trial_system_attrs = storage.get_trial_system_attrs(trial._trial_id)
trial_artifacts = list_trial_artifacts(study_system_attrs, trial_system_attrs, trial)
artifact_metas.extend(trial_artifacts)
for meta in artifact_metas:
@@ -339,7 +356,7 @@ def list_study_artifacts(study_system_attrs: dict[str, Any]) -> list[ArtifactMet
def list_trial_artifacts(
study_system_attrs: dict[str, Any], trial: FrozenTrial
study_system_attrs: dict[str, Any], trial_system_attrs: dict[str, Any], trial: FrozenTrial
) -> list[ArtifactMeta]:
# Collect ArtifactMeta from study_system_attrs due to backward compatibility.
dashboard_artifact_metas = [
@@ -353,7 +370,7 @@ def list_trial_artifacts(
# See https://github.com/optuna/optuna/blob/f827582a8/optuna/artifacts/_upload.py#L16
optuna_artifact_metas = [
json.loads(value)
for key, value in trial.system_attrs.items()
for key, value in trial_system_attrs.items()
if key.startswith(ARTIFACTS_ATTR_PREFIX)
]
artifact_metas = dashboard_artifact_metas + optuna_artifact_metas
+2 -1
View File
@@ -338,7 +338,8 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler):
trial: optuna.trial.FrozenTrial,
search_space: dict[str, optuna.distributions.BaseDistribution],
) -> dict[str, Any]:
preferences = get_preferences(study.system_attrs)
study_system_attrs = study._storage.get_study_system_attrs(study._study_id)
preferences = get_preferences(study_system_attrs)
if len(preferences) == 0 or len(search_space) == 0:
return {}
+11
View File
@@ -29,8 +29,10 @@ import {
isFileUploading,
fetchedTrialsPartiallyState,
artifactIsAvailable,
plotlypyIsAvailableState,
reloadIntervalState,
trialsUpdatingState,
studySummariesLoadingState,
} from "./state"
import { getDominatedTrials } from "./dominatedTrials"
@@ -49,6 +51,11 @@ export const actionCreator = () => {
const setArtifactIsAvailable = useSetRecoilState<boolean>(artifactIsAvailable)
const setFetchedTrialsPartially = useSetRecoilState<boolean>(
fetchedTrialsPartiallyState
const setPlotlypyIsAvailable = useSetRecoilState<boolean>(
plotlypyIsAvailableState
)
const setStudySummariesLoading = useSetRecoilState<boolean>(
studySummariesLoadingState
)
const setStudyDetailState = (studyId: number, study: StudyDetail) => {
@@ -215,12 +222,15 @@ export const actionCreator = () => {
const updateAPIMeta = () => {
getMetaInfoAPI().then((r) => {
setArtifactIsAvailable(r.artifact_is_available)
setPlotlypyIsAvailable(r.plotlypy_is_available)
})
}
const updateStudySummaries = (successMsg?: string) => {
setStudySummariesLoading(true)
getStudySummariesAPI()
.then((studySummaries: StudySummary[]) => {
setStudySummariesLoading(false)
setStudySummaries(studySummaries)
if (successMsg) {
@@ -228,6 +238,7 @@ export const actionCreator = () => {
}
})
.catch((err) => {
setStudySummariesLoading(false)
enqueueSnackbar(`Failed to fetch study list.`, {
variant: "error",
})
+36
View File
@@ -1,9 +1,11 @@
import * as plotly from "plotly.js-dist-min"
import axios from "axios"
const axiosInstance = axios.create({ baseURL: API_ENDPOINT })
type APIMeta = {
artifact_is_available: boolean
plotlypy_is_available: boolean
}
export const getMetaInfoAPI = (): Promise<APIMeta> => {
@@ -443,3 +445,37 @@ export const reportFeedbackComponentAPI = (
return
})
}
type PlotResponse = {
data: plotly.Data[]
layout: plotly.Layout
}
export enum PlotType {
Contour = "contour",
Slice = "slice",
ParallelCoordinate = "parallel_coordinate",
Rank = "rank",
EDF = "edf",
}
export const getPlotAPI = (
studyId: number,
plotType: PlotType
): Promise<PlotResponse> => {
return axiosInstance
.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)
}
@@ -7,11 +7,11 @@ import {
Button,
DialogActions,
} from "@mui/material"
import { actionCreator } from "../action"
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()
@@ -20,7 +20,7 @@ import DownloadIcon from "@mui/icons-material/Download"
import DeleteIcon from "@mui/icons-material/Delete"
import FullscreenIcon from "@mui/icons-material/Fullscreen"
import { actionCreator } from "../action"
import { actionCreator } from "../../action"
import { useDeleteStudyArtifactDialog } from "./DeleteArtifactDialog"
import {
useThreejsArtifactModal,
@@ -115,7 +115,7 @@ export const ThreejsArtifactViewer: React.FC<ThreejsArtifactViewerProps> = (
{geometry.length > 0 &&
geometry.map((geo, index) => (
<mesh key={index} geometry={geo}>
<meshNormalMaterial />
<meshNormalMaterial side={THREE.DoubleSide} />
</mesh>
))}
</Canvas>
@@ -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])
@@ -20,7 +20,7 @@ import DownloadIcon from "@mui/icons-material/Download"
import DeleteIcon from "@mui/icons-material/Delete"
import FullscreenIcon from "@mui/icons-material/Fullscreen"
import { actionCreator } from "../action"
import { actionCreator } from "../../action"
import { useDeleteTrialArtifactDialog } from "./DeleteArtifactDialog"
import {
useThreejsArtifactModal,
@@ -0,0 +1,71 @@
import React, { useCallback, useEffect, useState, useRef } from "react"
import WaveSurfer from "wavesurfer.js"
import { Box } from "@mui/material"
interface WaveSurferArtifactViewerProps {
height: number
waveColor: string
progressColor: string
url: string
}
const useWavesurfer = (
containerRef: React.MutableRefObject<HTMLDivElement>,
options: WaveSurferArtifactViewerProps
) => {
const [wavesurfer, setWavesurfer] = useState<WaveSurfer | null>(null)
useEffect(() => {
if (!containerRef.current) return
const ws = WaveSurfer.create({
...options,
container: containerRef.current,
})
setWavesurfer(ws)
return () => {
ws.destroy()
}
}, [containerRef])
return wavesurfer
}
// 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)
const onPlayClick = useCallback(() => {
if (!wavesurfer) return
wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play()
}, [wavesurfer])
useEffect(() => {
if (!wavesurfer) return
setIsPlaying(false)
const subscriptions = [
wavesurfer.on("play", () => setIsPlaying(true)),
wavesurfer.on("pause", () => setIsPlaying(false)),
]
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>
)
}
@@ -29,12 +29,8 @@ import { studySummariesState, studyDetailsState } from "../state"
import { AppDrawer } from "./AppDrawer"
import { GraphEdf } from "./GraphEdf"
import { GraphHistory } from "./GraphHistory"
import { useNavigate, useLocation } from "react-router-dom"
const useQuery = (): URLSearchParams => {
const { search } = useLocation()
return useMemo(() => new URLSearchParams(search), [search])
}
import { useNavigate } from "react-router-dom"
import { useQuery } from "../urlQuery"
const useQueriedStudies = (
studies: StudySummary[],
+64 -13
View File
@@ -8,10 +8,12 @@ import {
TablePagination,
TableRow,
TableSortLabel,
TextField,
Collapse,
IconButton,
Menu,
MenuItem,
Box,
} from "@mui/material"
import { styled } from "@mui/system"
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"
@@ -69,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) => {
@@ -84,6 +86,41 @@ function DataGrid<T>(props: {
setPage(0)
}
const PaginationForm: React.FC<{
onPageNumberSubmit: (value: number) => void
maxPageNumber: number
}> = ({ onPageNumberSubmit, maxPageNumber }) => {
// This component is separated from DataGrid to prevent `DataGrid` from re-rendering the page,
// every time any letters are input.
const [specifiedPageText, setSpecifiedPageText] = React.useState("")
const handleSubmitPageNumber = (
event: React.FormEvent<HTMLFormElement>
) => {
event.preventDefault()
const newPageNumber = parseInt(specifiedPageText, 10)
// Page is 0-indexed in `TablePagination`.
onPageNumberSubmit(newPageNumber - 1)
setSpecifiedPageText("") // reset the input field
}
return (
<form onSubmit={handleSubmitPageNumber}>
<TextField
size="small"
label={`Go to Page: n / ${maxPageNumber}`}
value={specifiedPageText}
type="number"
style={{ width: 200 }}
inputProps={{ min: 1, max: maxPageNumber }}
onChange={(e) => {
setSpecifiedPageText(e.target.value)
}}
/>
</form>
)
}
// Filtering
const filteredRows = rows.filter((row, rowIdx) => {
if (defaultFilter !== undefined && defaultFilter(row)) {
@@ -119,6 +156,7 @@ function DataGrid<T>(props: {
const RootDiv = styled("div")({
width: "100%",
})
const maxPageNumber = Math.ceil(filteredRows.length / rowsPerPage)
return (
<RootDiv>
<TableContainer>
@@ -177,15 +215,28 @@ function DataGrid<T>(props: {
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={rowsPerPageOption}
component="div"
count={filteredRows.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
{filteredRows.length > 0 ? (
<>
{/* @ts-ignore */}
<Box display="flex" alignItems="center">
<TablePagination
rowsPerPageOptions={rowsPerPageOption}
component="div"
count={filteredRows.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
{maxPageNumber > 2 ? (
<PaginationForm
onPageNumberSubmit={(page) => setPage(page)}
maxPageNumber={maxPageNumber}
/>
) : null}
</Box>
</>
) : null}
</RootDiv>
)
}
@@ -270,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,11 +15,44 @@ import blue from "@mui/material/colors/blue"
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
import { useMergedUnionSearchSpace } from "../searchSpace"
import { getAxisInfo } from "../graphUtil"
import { getPlotAPI, PlotType } from "../apiClient"
import { useBackendRender } from "../state"
const plotDomId = "graph-contour"
export const Contour: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
if (useBackendRender()) {
return <ContourBackend study={study} />
} else {
return <ContourFrontend study={study} />
}
}
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
}
getPlotAPI(studyId, PlotType.Contour)
.then(({ data, layout }) => {
plotly.react(plotDomId, data, layout)
})
.catch((err) => {
console.error(err)
})
}, [studyId, numCompletedTrials])
return <Box id={plotDomId} sx={{ height: "450px" }} />
}
const ContourFrontend: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const theme = useTheme()
const [objectiveId, setObjectiveId] = useState<number>(0)
@@ -3,6 +3,8 @@ import React, { FC, useEffect, useMemo } from "react"
import { Typography, useTheme, Box } from "@mui/material"
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
import { Target, useFilteredTrialsFromStudies } from "../trialFilter"
import { getCompareStudiesPlotAPI, CompareStudiesPlotType } from "../apiClient"
import { useBackendRender } from "../state"
const getPlotDomId = (objectiveId: number) => `graph-edf-${objectiveId}`
@@ -14,6 +16,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 domId = getPlotDomId(objectiveId)
@@ -21,6 +21,7 @@ import {
Target,
useObjectiveAndUserAttrTargetsFromStudies,
} from "../trialFilter"
import { useNavigate } from "react-router-dom"
const plotDomId = "graph-history"
@@ -37,6 +38,7 @@ export const GraphHistory: FC<{
includePruned: boolean
}> = ({ studies, logScale, includePruned }) => {
const theme = useTheme()
const navigate = useNavigate()
const [xAxis, setXAxis] = useState<
"number" | "datetime_start" | "datetime_complete"
>("number")
@@ -69,6 +71,41 @@ export const GraphHistory: FC<{
theme.palette.mode,
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, markerSize])
const handleObjectiveChange = (event: SelectChangeEvent<string>) => {
@@ -214,8 +251,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>[] = []
@@ -95,8 +95,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 [targets, searchSpace, renderCheckBoxes] = useTargets(study)
@@ -13,6 +13,7 @@ import {
} from "@mui/material"
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
import { makeHovertext } from "../graphUtil"
import { useNavigate } from "react-router-dom"
const plotDomId = "graph-pareto-front"
@@ -20,6 +21,7 @@ export const GraphParetoFront: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const theme = useTheme()
const navigate = useNavigate()
const [objectiveXId, setObjectiveXId] = useState<number>(0)
const [objectiveYId, setObjectiveYId] = useState<number>(1)
const objectiveNames: string[] = study?.objective_names || []
@@ -35,6 +37,23 @@ export const GraphParetoFront: FC<{
useEffect(() => {
if (study != null) {
plotParetoFront(study, objectiveXId, objectiveYId, theme.palette.mode)
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])
@@ -188,12 +207,12 @@ const getIsDominated2D = (normalizedValues: number[][]) => {
a[0] > b[0]
? 1
: a[0] < b[0]
? -1
: a[1] > b[1]
? 1
: a[1] < b[1]
? -1
: 0
? -1
: a[1] > b[1]
? 1
: a[1] < b[1]
? -1
: 0
)
let maxValueSeen0 = sorted[0][0]
let minValueSeen1 = sorted[0][1]
+35 -2
View File
@@ -14,6 +14,8 @@ import {
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
import { getAxisInfo, makeHovertext } from "../graphUtil"
import { useMergedUnionSearchSpace } from "../searchSpace"
import { getPlotAPI, PlotType } from "../apiClient"
import { useBackendRender } from "../state"
const plotDomId = "graph-rank"
@@ -31,6 +33,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 [objectiveId, setobjectiveId] = useState<number>(0)
@@ -157,8 +190,8 @@ const getRankPlotInfo = (
return typeof value === "number"
? value
: value.includes("-")
? -Infinity
: Infinity
? -Infinity
: Infinity
}
filteredTrials.forEach((trial, i) => {
const xValue = xAxis.values[i]
@@ -20,6 +20,8 @@ import {
useParamTargets,
} from "../trialFilter"
import { useMergedUnionSearchSpace } from "../searchSpace"
import { getPlotAPI, PlotType } from "../apiClient"
import { useBackendRender } from "../state"
const plotDomId = "graph-slice"
@@ -32,6 +34,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()
@@ -56,6 +56,7 @@ const plotTimeline = (trials: Trial[], mode: string) => {
Running: "green",
Waiting: "gray",
}
const runningKey = "Running"
const lastTrials = trials.slice(-maxBars) // To only show last elements
const minDatetime = new Date(
@@ -65,13 +66,38 @@ const plotTimeline = (trials: Trial[], mode: string) => {
)
)
)
const maxDatetime = new Date(
Math.max(
...lastTrials.map(
(t) => t.datetime_start?.getTime() ?? minDatetime.getTime()
)
)
const maxRunDuration = Math.max(
...trials.map((t) => {
return t.datetime_start === undefined || t.datetime_complete === undefined
? -Infinity
: t.datetime_complete.getTime() - t.datetime_start.getTime()
})
)
const hasRunning =
(maxRunDuration === -Infinity &&
trials.some((t) => t.state === runningKey)) ||
trials.some((t) => {
if (t.state !== runningKey) {
return false
}
const now = new Date().getTime()
const start = t.datetime_start?.getTime() ?? now
// This is an ad-hoc handling to check if the trial is running.
// We do not check via `trialState` because some trials may have state=RUNNING,
// even if they are not running because of unexpected job kills.
// In this case, we would like to ensure that these trials will not squash the timeline plot
// for the other trials.
return now - start < maxRunDuration * 5
})
const maxDatetime = hasRunning
? new Date()
: new Date(
Math.max(
...lastTrials.map(
(t) => t.datetime_complete?.getTime() ?? minDatetime.getTime()
)
)
)
const layout: Partial<plotly.Layout> = {
margin: {
l: 50,
@@ -93,11 +119,20 @@ const plotTimeline = (trials: Trial[], mode: string) => {
}
const makeTrace = (bars: Trial[], state: string, color: string) => {
const starts = bars.map((b) => b.datetime_start ?? new Date())
const completes = bars.map((b, i) => b.datetime_complete ?? starts[i])
const isRunning = state === runningKey
// Waiting trials should not squash other trials, so use `maxDatetime` instead of `new Date()`.
const starts = bars.map((b) => b.datetime_start ?? maxDatetime)
const runDurations = bars.map((b, i) => {
const startTime = starts[i].getTime()
const completeTime = isRunning
? maxDatetime.getTime()
: b.datetime_complete?.getTime() ?? startTime
// By using 1 as the min value, we can recognize these bars at least when zooming in.
return Math.max(1, completeTime - startTime)
})
const trace: Partial<plotly.PlotData> = {
type: "bar",
x: starts.map((s, i) => completes[i].getTime() - s.getTime()),
x: runDurations,
y: bars.map((b) => b.number),
// @ts-ignore: To suppress ts(2322)
base: starts.map((s) => s.toISOString()),
@@ -8,10 +8,10 @@ import {
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 { DataGrid, DataGridColumn } from "../DataGrid"
import { BestTrialsCard } from "../BestTrialsCard"
import { useStudyDetailValue, useStudySummaryValue } from "../../state"
import { Contour } from "../GraphContour"
export const PreferentialAnalytics: FC<{ studyId: number }> = ({ studyId }) => {
const theme = useTheme()
@@ -23,7 +23,7 @@ import "reactflow/dist/style.css"
import ELK from "elkjs/lib/elk.bundled.js"
import { ElkNode } from "elkjs/lib/elk-api.js"
import { useStudyDetailValue } from "../state"
import { useStudyDetailValue } from "../../state"
import { getArtifactUrlPath } from "./PreferentialTrials"
import { PreferentialOutputComponent } from "./PreferentialOutputComponent"
@@ -15,11 +15,11 @@ import DeleteIcon from "@mui/icons-material/Delete"
import Modal from "@mui/material/Modal"
import { red } from "@mui/material/colors"
import { TrialListDetail } from "./TrialList"
import { TrialListDetail } from "../TrialList"
import { getArtifactUrlPath } from "./PreferentialTrials"
import { formatDate } from "../dateUtil"
import { actionCreator } from "../action"
import { useStudyDetailValue } from "../state"
import { formatDate } from "../../dateUtil"
import { actionCreator } from "../../action"
import { useStudyDetailValue } from "../../state"
import { PreferentialOutputComponent } from "./PreferentialOutputComponent"
type TrialType = "worst" | "none"
@@ -252,7 +252,7 @@ const ChoiceTrials: FC<{
)
}
export const PreferenceHistory: FC<{ studyDetail: StudyDetail | null }> = ({
export const PreferentialHistory: FC<{ studyDetail: StudyDetail | null }> = ({
studyDetail,
}) => {
if (
@@ -1,6 +1,6 @@
import React, { FC, useMemo } from "react"
import { ArtifactCardMedia } from "./ArtifactCardMedia"
import { MarkdownRenderer } from "./Note"
import { ArtifactCardMedia } from "../Artifact/ArtifactCardMedia"
import { MarkdownRenderer } from "../Note"
export const PreferentialOutputComponent: FC<{
trial: Trial
@@ -27,12 +27,12 @@ import ClearIcon from "@mui/icons-material/Clear"
import SettingsIcon from "@mui/icons-material/Settings"
import FullscreenIcon from "@mui/icons-material/Fullscreen"
import { actionCreator } from "../action"
import { TrialListDetail } from "./TrialList"
import { actionCreator } from "../../action"
import { TrialListDetail } from "../TrialList"
import {
isThreejsArtifact,
useThreejsArtifactModal,
} from "./ThreejsArtifactViewer"
} from "../Artifact/ThreejsArtifactViewer"
import { PreferentialOutputComponent } from "./PreferentialOutputComponent"
const SettingsPage: FC<{
+5 -32
View File
@@ -12,7 +12,6 @@ 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"
@@ -32,10 +31,10 @@ import { GraphEdf } from "./GraphEdf"
import { GraphRank } from "./GraphRank"
import { TrialList } from "./TrialList"
import { StudyHistory } from "./StudyHistory"
import { PreferentialTrials } from "./PreferentialTrials"
import { PreferenceHistory } from "./PreferenceHistory"
import { PreferentialAnalytics } from "./PreferentialAnalytics"
import { PreferentialGraph } from "./PreferentialGraph"
import { PreferentialTrials } from "./Preferential/PreferentialTrials"
import { PreferentialHistory } from "./Preferential/PreferentialHistory"
import { PreferentialAnalytics } from "./Preferential/PreferentialAnalytics"
import { PreferentialGraph } from "./Preferential/PreferentialGraph"
interface ParamTypes {
studyId: string
@@ -158,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} />
@@ -229,7 +202,7 @@ export const StudyDetail: FC<{
</Box>
)
} else if (page === "preferenceHistory") {
content = <PreferenceHistory studyDetail={studyDetail} />
content = <PreferentialHistory studyDetail={studyDetail} />
}
const toolbar = (
@@ -17,7 +17,7 @@ import { DataGrid, DataGridColumn } from "./DataGrid"
import { GraphHyperparameterImportance } from "./GraphHyperparameterImportances"
import { UserDefinedPlot } from "./UserDefinedPlot"
import { BestTrialsCard } from "./BestTrialsCard"
import { StudyArtifactCards } from "./StudyArtifactCards"
import { StudyArtifactCards } from "./Artifact/StudyArtifactCards"
import { useRecoilValue } from "recoil"
import {
useStudyDetailValue,
+72 -61
View File
@@ -1,5 +1,5 @@
import React, { FC, useEffect, useMemo, useState } from "react"
import { useNavigate, useLocation } from "react-router-dom"
import React, { FC, useEffect, useState } from "react"
import { useNavigate } from "react-router-dom"
import { useRecoilValue } from "recoil"
import { Link } from "react-router-dom"
import {
@@ -18,7 +18,7 @@ import {
TextField,
CardActions,
} from "@mui/material"
import { Delete, Refresh, Search } from "@mui/icons-material"
import { Delete, Refresh, Search, HourglassTop } from "@mui/icons-material"
import SortIcon from "@mui/icons-material/Sort"
import HomeIcon from "@mui/icons-material/Home"
import AddBoxIcon from "@mui/icons-material/AddBox"
@@ -27,12 +27,13 @@ import DriveFileRenameOutlineIcon from "@mui/icons-material/DriveFileRenameOutli
import { actionCreator } from "../action"
import { DebouncedInputTextField } from "./Debounce"
import { studySummariesState } from "../state"
import { studySummariesLoadingState, studySummariesState } from "../state"
import { styled } from "@mui/system"
import { AppDrawer } from "./AppDrawer"
import { useCreateStudyDialog } from "./CreateStudyDialog"
import { useDeleteStudyDialog } from "./DeleteStudyDialog"
import { useRenameStudyDialog } from "./RenameStudyDialog"
import { useQuery } from "../urlQuery"
export const StudyList: FC<{
toggleColorMode: () => void
@@ -57,12 +58,9 @@ export const StudyList: FC<{
useDeleteStudyDialog()
const [openRenameStudyDialog, renderRenameStudyDialog] =
useRenameStudyDialog(studies)
const isLoading = useRecoilValue<boolean>(studySummariesLoadingState)
const navigate = useNavigate()
const useQuery = (): URLSearchParams => {
const { search } = useLocation()
return useMemo(() => new URLSearchParams(search), [search])
}
const query = useQuery()
const initialSortBy = query.get("studies_order_by") === "asc" ? "asc" : "desc"
const [sortBy, setSortBy] = useState<"asc" | "desc">(initialSortBy)
@@ -72,7 +70,6 @@ export const StudyList: FC<{
if (sortBy === "desc") {
filteredStudies = filteredStudies.reverse()
}
useEffect(() => {
action.updateStudySummaries()
}, [])
@@ -126,6 +123,71 @@ export const StudyList: FC<{
const toolbar = <HomeIcon sx={{ margin: theme.spacing(0, 1) }} />
let studyListContent
if (isLoading) {
studyListContent = (
<Box sx={{ margin: theme.spacing(2) }}>
<SvgIcon fontSize="small" color="action">
<HourglassTop />
</SvgIcon>
Loading studies...
</Box>
)
} else {
studyListContent = filteredStudies.map((study) => (
<Card
key={study.study_id}
sx={{ margin: theme.spacing(2), width: "500px" }}
>
<CardActionArea
component={Link}
to={`${URL_PREFIX}/studies/${study.study_id}`}
>
<CardContent>
<Typography variant="h5">
{study.study_id}. {study.study_name}
</Typography>
<Typography
variant="subtitle1"
color="text.secondary"
component="div"
>
{study.is_preferential
? "Preferential Optimization"
: "Direction: " +
study.directions
.map((d) => d.toString().toUpperCase())
.join(", ")}
</Typography>
</CardContent>
</CardActionArea>
<CardActions disableSpacing sx={{ paddingTop: 0 }}>
<Box sx={{ flexGrow: 1 }} />
<IconButton
aria-label="rename study"
size="small"
color="inherit"
onClick={() => {
openRenameStudyDialog(study.study_id, study.study_name)
}}
>
<DriveFileRenameOutlineIcon />
</IconButton>
<IconButton
aria-label="delete study"
size="small"
color="inherit"
onClick={() => {
openDeleteStudyDialog(study.study_id)
}}
>
<Delete />
</IconButton>
</CardActions>
</Card>
))
}
return (
<Box sx={{ display: "flex" }}>
<AppDrawer toggleColorMode={toggleColorMode} toolbar={toolbar}>
@@ -196,58 +258,7 @@ export const StudyList: FC<{
</CardContent>
</Card>
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
{filteredStudies.map((study) => (
<Card
key={study.study_id}
sx={{ margin: theme.spacing(2), width: "500px" }}
>
<CardActionArea
component={Link}
to={`${URL_PREFIX}/studies/${study.study_id}`}
>
<CardContent>
<Typography variant="h5">
{study.study_id}. {study.study_name}
</Typography>
<Typography
variant="subtitle1"
color="text.secondary"
component="div"
>
{study.is_preferential
? "Preferential Optimization"
: "Direction: " +
study.directions
.map((d) => d.toString().toUpperCase())
.join(", ")}
</Typography>
</CardContent>
</CardActionArea>
<CardActions disableSpacing sx={{ paddingTop: 0 }}>
<Box sx={{ flexGrow: 1 }} />
<IconButton
aria-label="rename study"
size="small"
color="inherit"
onClick={() => {
openRenameStudyDialog(study.study_id, study.study_name)
}}
>
<DriveFileRenameOutlineIcon />
</IconButton>
<IconButton
aria-label="delete study"
size="small"
color="inherit"
onClick={() => {
openDeleteStudyDialog(study.study_id)
}}
>
<Delete />
</IconButton>
</CardActions>
</Card>
))}
{studyListContent}
</Box>
</Container>
</AppDrawer>
@@ -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) {
+3 -8
View File
@@ -21,13 +21,14 @@ import CheckBoxIcon from "@mui/icons-material/CheckBox"
import StopCircleIcon from "@mui/icons-material/StopCircle"
import { TrialNote } from "./Note"
import { useNavigate, useLocation } from "react-router-dom"
import { useNavigate } from "react-router-dom"
import ListItemIcon from "@mui/material/ListItemIcon"
import { useRecoilValue } from "recoil"
import { artifactIsAvailable } from "../state"
import { actionCreator } from "../action"
import { TrialFormWidgets } from "./TrialFormWidgets"
import { TrialArtifactCards } from "./TrialArtifactCards"
import { TrialArtifactCards } from "./Artifact/TrialArtifactCards"
import { useQuery } from "../urlQuery"
const states: TrialState[] = [
"Complete",
@@ -61,12 +62,6 @@ const getChipColor = (state: TrialState): Color => {
return "default"
}
const useQuery = (): URLSearchParams => {
const { search } = useLocation()
return useMemo(() => new URLSearchParams(search), [search])
}
const useExcludedStates = (query: URLSearchParams): TrialState[] => {
return useMemo(() => {
const exclude = query.get("exclude")
+21 -8
View File
@@ -1,6 +1,7 @@
import React, { FC } from "react"
import { IconButton } from "@mui/material"
import { IconButton, Button, useTheme } from "@mui/material"
import LinkIcon from "@mui/icons-material/Link"
import DownloadIcon from "@mui/icons-material/Download"
import { DataGridColumn, DataGrid } from "./DataGrid"
import { Link } from "react-router-dom"
@@ -9,6 +10,7 @@ export const TrialTable: FC<{
studyDetail: StudyDetail | null
initialRowsPerPage?: number
}> = ({ studyDetail, initialRowsPerPage }) => {
const theme = useTheme()
const trials: Trial[] = studyDetail !== null ? studyDetail.trials : []
const objectiveNames: string[] = studyDetail?.objective_names || []
@@ -190,12 +192,23 @@ export const TrialTable: FC<{
})
return (
<DataGrid<Trial>
columns={columns}
rows={trials}
keyField={"trial_id"}
dense={true}
initialRowsPerPage={initialRowsPerPage}
/>
<>
<DataGrid<Trial>
columns={columns}
rows={trials}
keyField={"trial_id"}
dense={true}
initialRowsPerPage={initialRowsPerPage}
/>
<Button
variant="outlined"
startIcon={<DownloadIcon />}
download
href={`/csv/${studyDetail?.id}`}
sx={{ marginRight: theme.spacing(2), minWidth: "120px" }}
>
Download CSV File
</Button>
</>
)
}
@@ -1,72 +0,0 @@
import React, { useCallback, useEffect, useState, useRef } from "react"
import WaveSurfer from "wavesurfer.js"
import { Box } from "@mui/material"
interface WaveSurferArtifactViewerProps {
height: number
waveColor: string
progressColor: string
url: string
}
const useWavesurfer = (
containerRef: React.MutableRefObject<HTMLDivElement>,
options: WaveSurferArtifactViewerProps
) => {
const [wavesurfer, setWavesurfer] = useState<WaveSurfer | null>(null)
useEffect(() => {
if (!containerRef.current) return
const ws = WaveSurfer.create({
...options,
container: containerRef.current,
})
setWavesurfer(ws)
return () => {
ws.destroy()
}
}, [containerRef])
return wavesurfer
}
// 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)
const onPlayClick = useCallback(() => {
if (!wavesurfer) return
wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play()
}, [wavesurfer])
useEffect(() => {
if (!wavesurfer) return
setIsPlaying(false)
const subscriptions = [
wavesurfer.on("play", () => setIsPlaying(true)),
wavesurfer.on("pause", () => setIsPlaying(false)),
]
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>
)
}
+2 -2
View File
@@ -47,8 +47,8 @@ const getAxisInfoForCategoricalParams = (
a.toLowerCase() < b.toLowerCase()
? -1
: a.toLowerCase() > b.toLowerCase()
? 1
: 0
? 1
: 0
)
return {
name: paramName,
+26
View File
@@ -1,4 +1,5 @@
import { atom, useRecoilValue } from "recoil"
import { useQuery } from "./urlQuery"
export const studySummariesState = atom<StudySummary[]>({
key: "studySummaries",
@@ -48,6 +49,16 @@ export const artifactIsAvailable = atom<boolean>({
default: false,
})
export const plotlypyIsAvailableState = atom<boolean>({
key: "plotlypyIsAvailable",
default: true,
})
export const studySummariesLoadingState = atom<boolean>({
key: "studySummariesLoadingState",
default: false,
})
export const useStudyDetailValue = (studyId: number): StudyDetail | null => {
const studyDetails = useRecoilValue<StudyDetails>(studyDetailsState)
return studyDetails[studyId] || null
@@ -99,3 +110,18 @@ export const useArtifacts = (studyId: number, trialId: number): Artifact[] => {
}
return trial.artifacts
}
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
}
+7
View File
@@ -0,0 +1,7 @@
import { useLocation } from "react-router-dom"
import { useMemo } from "react"
export const useQuery = (): URLSearchParams => {
const { search } = useLocation()
return useMemo(() => new URLSearchParams(search), [search])
}
+8553 -7942
View File
File diff suppressed because it is too large Load Diff
+46 -46
View File
@@ -5,10 +5,10 @@
"description": "Dashboard for Optuna",
"main": "index.js",
"scripts": {
"fmt": "prettier --write \"{optuna_dashboard/ts,typescript_tests,standalone_app/src,vscode/src}/**/*.{ts,tsx}\"",
"fmt": "biome format --write .",
"lint": "npm run lint:eslint && npm run lint:fmt",
"lint:eslint": "eslint . --ext .ts,.tsx --max-warnings 0",
"lint:fmt": "prettier --list-different \"{optuna_dashboard/ts,typescript_tests,standalone_app/src,vscode/src}/**/*.{ts,tsx}\"",
"lint:fmt": "biome format .",
"watch": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack --watch",
"build": "webpack",
"build:dev": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack",
@@ -18,56 +18,56 @@
"author": "Masashi Shibata",
"license": "MIT",
"dependencies": {
"@emotion/react": "^11.10.8",
"@emotion/styled": "^11.10.8",
"@mui/icons-material": "^5.11.6",
"@mui/lab": "^5.0.0-alpha.128",
"@mui/material": "^5.12.1",
"@react-three/drei": "^9.80.0",
"@react-three/fiber": "^8.13.6",
"@types/three": "^0.154.0",
"axios": "^1.6.0",
"elkjs": "^0.8.2",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.6",
"@mui/lab": "^5.0.0-alpha.162",
"@mui/material": "^5.15.6",
"@react-three/drei": "^9.96.4",
"@react-three/fiber": "^8.15.15",
"@types/three": "^0.160.0",
"axios": "^1.6.7",
"elkjs": "^0.9.1",
"notistack": "^3.0.1",
"plotly.js-dist-min": "^2.22.0",
"plotly.js-dist-min": "^2.28.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.4",
"react-router-dom": "^6.11.0",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.21.3",
"react-syntax-highlighter": "^15.5.0",
"reactflow": "^11.8.3",
"reactflow": "^11.10.3",
"recoil": "^0.7.7",
"rehype-mathjax": "^4.0.2",
"rehype-raw": "^6.1.1",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"three": "^0.155.0",
"wavesurfer.js": "^7.4.12"
"rehype-mathjax": "^6.0.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"three": "^0.160.1",
"wavesurfer.js": "^7.7.0"
},
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/preset-env": "^7.14.5",
"@testing-library/react": "^13.4.0",
"@types/jest": "^29.2.1",
"@types/plotly.js": "^2.12.11",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"@types/react-syntax-highlighter": "^15.5.5",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"compression-webpack-plugin": "^10.0.0",
"css-loader": "^6.8.1",
"esbuild-loader": "^2.18.0",
"eslint": "^8.53.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",
"webpack": "^5.76.0",
"webpack-cli": "^4.9.2"
"@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",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@types/react-syntax-highlighter": "^15.5.11",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"compression-webpack-plugin": "^11.0.0",
"css-loader": "^6.9.1",
"esbuild-loader": "^4.0.3",
"eslint": "^8.56.0",
"jest": "^29.7.0",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.7.0",
"style-loader": "^3.3.4",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",
"webpack": "^5.90.0",
"webpack-cli": "^5.1.4"
}
}
+5 -1
View File
@@ -27,7 +27,7 @@ classifiers = [
]
dependencies = [
"bottle",
"optuna>=2.4.0",
"optuna>=3.1.0",
"packaging",
"scikit-learn",
]
@@ -59,6 +59,10 @@ preferential = [
"botorch>=0.8.1",
]
plotlypy = [
"plotly",
]
[project.scripts]
optuna-dashboard = "optuna_dashboard._cli:main"
+20 -3
View File
@@ -4,6 +4,13 @@ import tempfile
from unittest.mock import MagicMock
import optuna
from packaging import version
import pytest
if version.parse(optuna.__version__) < version.parse("3.3.0"):
pytest.skip("optuna.artiracts module is introduced at v3.3.0", allow_module_level=True)
from optuna.artifacts import FileSystemArtifactStore
from optuna.artifacts import upload_artifact
from optuna.storages import BaseStorage
@@ -12,7 +19,6 @@ from optuna_dashboard.artifact import _backend
from optuna_dashboard.artifact import upload_artifact as dashboard_upload_artifact
from optuna_dashboard.artifact._backend_to_store import to_artifact_store
from optuna_dashboard.artifact.file_system import FileSystemBackend
import pytest
from ..wsgi_client import send_request
@@ -84,9 +90,12 @@ def test_delete_all_artifacts(init_storage_with_artifact_meta: MagicMock) -> Non
def test_list_trial_artifacts(init_storage_with_artifact_meta: MagicMock) -> None:
storage = init_storage_with_artifact_meta
trial = MagicMock(_trial_id=0, system_attrs=storage.get_trial_system_attrs(0))
trial_system_attrs = storage.get_trial_system_attrs(0)
trial = MagicMock(_trial_id=0, system_attrs={})
actual = _backend.list_trial_artifacts(storage.get_study_system_attrs(0), trial)
actual = _backend.list_trial_artifacts(
storage.get_study_system_attrs(0), trial_system_attrs, trial
)
assert actual == [
{"artifact_id": "id0", "filename": "foo.txt"},
{"artifact_id": "id1", "filename": "bar.txt"},
@@ -119,6 +128,10 @@ def test_study_artifact_not_found() -> None:
assert status == 404
@pytest.mark.skipif(
version.parse(optuna.__version__) < version.parse("3.4.0"),
reason="upload_artiract needs storage",
)
def test_successful_study_artifact_retrieval() -> None:
storage = optuna.storages.InMemoryStorage()
study = optuna.create_study(storage=storage)
@@ -254,6 +267,10 @@ def test_upload_artifact() -> None:
assert data == "dummy_content"
@pytest.mark.skipif(
version.parse(optuna.__version__) < version.parse("3.4.0"),
reason="upload_artiract needs storage",
)
def test_delete_study_artifact() -> None:
storage = optuna.storages.InMemoryStorage()
study = optuna.create_study(storage=storage)
+7 -2
View File
@@ -2,12 +2,17 @@ import io
from unittest import TestCase
import boto3
from moto import mock_s3
from optuna_dashboard.artifact.boto3 import Boto3Backend
from optuna_dashboard.artifact.exceptions import ArtifactNotFound
@mock_s3
try:
from moto import mock_aws
except ImportError:
from moto import mock_s3 as mock_aws
@mock_aws
class Boto3BackendTestCase(TestCase):
def setUp(self) -> None:
self.s3_client = boto3.client("s3")
@@ -36,8 +36,11 @@ def test_list_optuna_trial_artifacts() -> None:
study.tell(trial, 0.0)
study_system_attrs = storage.get_study_system_attrs(study._study_id)
trial_system_attrs = storage.get_trial_system_attrs(trial._trial_id)
frozen_trial = storage.get_trial(trial._trial_id)
artifact_meta_list = list_trial_artifacts(study_system_attrs, frozen_trial)
artifact_meta_list = list_trial_artifacts(
study_system_attrs, trial_system_attrs, frozen_trial
)
assert len(artifact_meta_list) == 1
artifact_id = artifact_meta_list[0]["artifact_id"]
+4 -1
View File
@@ -11,6 +11,7 @@ from optuna.distributions import IntDistribution
from optuna.samplers import BaseSampler
from optuna.trial import TrialState
from optuna_dashboard.preferential import create_study
from packaging import version
import pytest
@@ -27,7 +28,9 @@ parametrize_sampler = pytest.mark.parametrize(
pytest.param(
PreferentialGPSampler,
marks=pytest.mark.skipif(
sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support"
sys.version_info < (3, 8)
or version.parse(optuna.__version__) < version.parse("3.2.0"),
reason="BoTorch dropped Python3.7 support",
),
),
],
+5
View File
@@ -8,6 +8,7 @@ from typing import Callable
from unittest.mock import patch
import uuid
import optuna
from optuna import copy_study
from optuna import create_trial
from optuna import delete_study
@@ -17,12 +18,16 @@ from optuna.exceptions import DuplicatedStudyError
from optuna.trial import TrialState
from optuna_dashboard.preferential import create_study
from optuna_dashboard.preferential import load_study
from packaging import version
import pytest
from ..storage_supplier import parametrize_storages
from ..storage_supplier import StorageSupplier
if version.parse(optuna.__version__) < version.parse("3.4.0"):
pytest.skip("Preferential optimization is introduced at v3.4.0", allow_module_level=True)
if sys.version_info < (3, 8):
pytest.skip("BoTorch dropped Python3.7 support", allow_module_level=True)
+29
View File
@@ -18,6 +18,7 @@ from optuna_dashboard._preferential_history import remove_history
from optuna_dashboard._preferential_history import report_history
from optuna_dashboard._serializer import serialize_preference_history
from optuna_dashboard.preferential import create_study
from packaging import version
import pytest
from .wsgi_client import send_request
@@ -94,6 +95,10 @@ class APITestCase(TestCase):
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(
version.parse(optuna.__version__) < version.parse("3.2.0"),
reason="Needs optuna.search_space",
)
def test_get_best_trials_of_preferential_study(self) -> None:
storage = optuna.storages.InMemoryStorage()
study = create_study(n_generate=4, storage=storage)
@@ -118,6 +123,10 @@ class APITestCase(TestCase):
assert best_trials[0]["number"] == 0
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
@pytest.mark.skipif(
version.parse(optuna.__version__) < version.parse("3.2.0"),
reason="Needs optuna.search_space",
)
def test_report_preference(self) -> None:
storage = optuna.storages.InMemoryStorage()
study = create_study(n_generate=4, storage=storage)
@@ -152,6 +161,10 @@ class APITestCase(TestCase):
assert worse.number == 1
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
@pytest.mark.skipif(
version.parse(optuna.__version__) < version.parse("3.2.0"),
reason="Needs optuna.search_space",
)
def test_report_preference_when_typo_mode(self) -> None:
storage = optuna.storages.InMemoryStorage()
study = create_study(storage=storage, n_generate=3)
@@ -176,6 +189,10 @@ class APITestCase(TestCase):
self.assertEqual(status, 400)
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
@pytest.mark.skipif(
version.parse(optuna.__version__) < version.parse("3.2.0"),
reason="Needs optuna.search_space",
)
def test_change_component(self) -> None:
storage = optuna.storages.InMemoryStorage()
study = create_study(storage=storage, n_generate=3)
@@ -311,6 +328,10 @@ class APITestCase(TestCase):
assert note_ver_key(0) not in study.system_attrs
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
@pytest.mark.skipif(
version.parse(optuna.__version__) < version.parse("3.2.0"),
reason="Needs optuna.search_space",
)
def test_skip_trial(self) -> None:
storage = optuna.storages.InMemoryStorage()
study = create_study(n_generate=4, storage=storage)
@@ -336,6 +357,10 @@ class APITestCase(TestCase):
assert best_trials[0].number == 2
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
@pytest.mark.skipif(
version.parse(optuna.__version__) < version.parse("3.2.0"),
reason="Needs optuna.search_space",
)
def test_remove_history(self) -> None:
storage = optuna.storages.InMemoryStorage()
study = create_study(storage=storage, n_generate=3)
@@ -370,6 +395,10 @@ class APITestCase(TestCase):
assert len(study.get_preferences()) == 0
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
@pytest.mark.skipif(
version.parse(optuna.__version__) < version.parse("3.2.0"),
reason="Needs optuna.search_space",
)
def test_restore_history(self) -> None:
storage = optuna.storages.InMemoryStorage()
study = create_study(storage=storage, n_generate=3)
@@ -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"},
@@ -5,6 +5,7 @@ import sys
from typing import Callable
from typing import TYPE_CHECKING
import optuna
from optuna.storages import BaseStorage
from optuna_dashboard._preferential_history import _SYSTEM_ATTR_PREFIX_HISTORY
from optuna_dashboard._preferential_history import NewHistory
@@ -14,6 +15,7 @@ from optuna_dashboard._preferential_history import restore_history
from optuna_dashboard._serializer import serialize_preference_history
from optuna_dashboard.preferential import create_study
from optuna_dashboard.preferential._system_attrs import _SYSTEM_ATTR_PREFIX_PREFERENCE
from packaging import version
import pytest
from .storage_supplier import parametrize_storages
@@ -24,6 +26,10 @@ if TYPE_CHECKING:
from optuna_dashboard._preferential_history import History
if version.parse(optuna.__version__) < version.parse("3.4.0"):
pytest.skip("Preferential optimization is introduced at v3.4.0", allow_module_level=True)
if sys.version_info < (3, 8):
pytest.skip("BoTorch dropped Python3.7 support", allow_module_level=True)
+7
View File
@@ -9,6 +9,7 @@ from optuna_dashboard._serializer import serialize_study_detail
from optuna_dashboard._serializer import serialize_study_summary
from optuna_dashboard._storage import get_study_summaries
from optuna_dashboard.preferential import create_study
from packaging import version
import pytest
@@ -53,6 +54,9 @@ def test_serialize_numpy_floating() -> None:
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
@pytest.mark.skipif(
version.parse(optuna.__version__) < version.parse("3.2.0"), reason="Needs optuna.search_space"
)
def test_get_study_detail_is_preferential() -> None:
storage = optuna.storages.InMemoryStorage()
study = create_study(n_generate=4, storage=storage)
@@ -80,6 +84,9 @@ def test_get_study_detail_is_not_preferential() -> None:
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
@pytest.mark.skipif(
version.parse(optuna.__version__) < version.parse("3.2.0"), reason="Needs optuna.search_space"
)
def test_get_study_summary_is_preferential() -> None:
storage = optuna.storages.InMemoryStorage()
create_study(n_generate=4, storage=storage)
+1 -1
View File
@@ -1,5 +1,5 @@
# project dependency
optuna>=2.4
optuna>=3.1
bottle
scikit-learn
streamlit
+2 -2
View File
@@ -65,8 +65,8 @@ function DataGrid<T>(props: {
initialRowsPerPage = initialRowsPerPage // use first element as default
? initialRowsPerPage
: isNumber(rowsPerPageOption[0])
? rowsPerPageOption[0]
: rowsPerPageOption[0].value
? rowsPerPageOption[0]
: rowsPerPageOption[0].value
const [rowsPerPage, setRowsPerPage] = React.useState(initialRowsPerPage)
const handleChangePage = (event: unknown, newPage: number) => {
@@ -231,8 +231,8 @@ const plotHistory = (
return xAxis === "number"
? trial.number
: xAxis === "datetime_start"
? trial.datetime_start ?? new Date()
: trial.datetime_complete ?? new Date()
? trial.datetime_start ?? new Date()
: trial.datetime_complete ?? new Date()
}
const getValue = (trial: Trial, objectiveId: number): number | null => {
+12 -12
View File
@@ -174,12 +174,12 @@ const getTrials = (
vals[2] === "COMPLETE"
? "Complete"
: vals[2] === "PRUNED"
? "Pruned"
: vals[2] === "RUNNING"
? "Running"
: vals[2] === "WAITING"
? "Waiting"
: "Fail"
? "Pruned"
: vals[2] === "RUNNING"
? "Running"
: vals[2] === "WAITING"
? "Waiting"
: "Fail"
const trial: Trial = {
trial_id: trialId,
number: vals[1],
@@ -220,8 +220,8 @@ const getTrialValues = (
vals[1] === "INF_NEG"
? "-inf"
: vals[1] === "INF_POS"
? "+inf"
: vals[0]
? "+inf"
: vals[0]
)
},
})
@@ -391,10 +391,10 @@ const getTrialIntermediateValues = (
vals[2] === "INF_NEG"
? "-inf"
: vals[2] === "INF_POS"
? "+inf"
: vals[2] === "NAN"
? "nan"
: vals[1],
? "+inf"
: vals[2] === "NAN"
? "nan"
: vals[1],
})
},
})