mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-10 12:23:22 +08:00
Merge branch 'main' into enhance/speedup-get-trials
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
optuna_dashboard/ts/components/PlotlyDarkMode.ts
|
||||
standalone_app/src/PlotlyDarkMode.ts
|
||||
@@ -1,4 +0,0 @@
|
||||
trailingComma: "es5"
|
||||
tabWidth: 2
|
||||
semi: false
|
||||
singleQuote: false
|
||||
+4
-4
@@ -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
@@ -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
@@ -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
@@ -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::
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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, []),
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+3
-3
@@ -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()
|
||||
|
||||
+1
-1
@@ -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,
|
||||
+2
-2
@@ -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])
|
||||
+1
-1
@@ -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[],
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()),
|
||||
|
||||
+4
-4
@@ -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()
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+5
-5
@@ -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 (
|
||||
+2
-2
@@ -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
|
||||
+3
-3
@@ -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<{
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -47,8 +47,8 @@ const getAxisInfoForCategoricalParams = (
|
||||
a.toLowerCase() < b.toLowerCase()
|
||||
? -1
|
||||
: a.toLowerCase() > b.toLowerCase()
|
||||
? 1
|
||||
: 0
|
||||
? 1
|
||||
: 0
|
||||
)
|
||||
return {
|
||||
name: paramName,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
Generated
+8553
-7942
File diff suppressed because it is too large
Load Diff
+46
-46
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
# project dependency
|
||||
optuna>=2.4
|
||||
optuna>=3.1
|
||||
bottle
|
||||
scikit-learn
|
||||
streamlit
|
||||
|
||||
@@ -65,8 +65,8 @@ function DataGrid<T>(props: {
|
||||
initialRowsPerPage = initialRowsPerPage // use first element as default
|
||||
? initialRowsPerPage
|
||||
: isNumber(rowsPerPageOption[0])
|
||||
? rowsPerPageOption[0]
|
||||
: rowsPerPageOption[0].value
|
||||
? rowsPerPageOption[0]
|
||||
: rowsPerPageOption[0].value
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(initialRowsPerPage)
|
||||
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
|
||||
@@ -231,8 +231,8 @@ const plotHistory = (
|
||||
return xAxis === "number"
|
||||
? trial.number
|
||||
: xAxis === "datetime_start"
|
||||
? trial.datetime_start ?? new Date()
|
||||
: trial.datetime_complete ?? new Date()
|
||||
? trial.datetime_start ?? new Date()
|
||||
: trial.datetime_complete ?? new Date()
|
||||
}
|
||||
|
||||
const getValue = (trial: Trial, objectiveId: number): number | null => {
|
||||
|
||||
@@ -174,12 +174,12 @@ const getTrials = (
|
||||
vals[2] === "COMPLETE"
|
||||
? "Complete"
|
||||
: vals[2] === "PRUNED"
|
||||
? "Pruned"
|
||||
: vals[2] === "RUNNING"
|
||||
? "Running"
|
||||
: vals[2] === "WAITING"
|
||||
? "Waiting"
|
||||
: "Fail"
|
||||
? "Pruned"
|
||||
: vals[2] === "RUNNING"
|
||||
? "Running"
|
||||
: vals[2] === "WAITING"
|
||||
? "Waiting"
|
||||
: "Fail"
|
||||
const trial: Trial = {
|
||||
trial_id: trialId,
|
||||
number: vals[1],
|
||||
@@ -220,8 +220,8 @@ const getTrialValues = (
|
||||
vals[1] === "INF_NEG"
|
||||
? "-inf"
|
||||
: vals[1] === "INF_POS"
|
||||
? "+inf"
|
||||
: vals[0]
|
||||
? "+inf"
|
||||
: vals[0]
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -391,10 +391,10 @@ const getTrialIntermediateValues = (
|
||||
vals[2] === "INF_NEG"
|
||||
? "-inf"
|
||||
: vals[2] === "INF_POS"
|
||||
? "+inf"
|
||||
: vals[2] === "NAN"
|
||||
? "nan"
|
||||
: vals[1],
|
||||
? "+inf"
|
||||
: vals[2] === "NAN"
|
||||
? "nan"
|
||||
: vals[1],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user