Merge branch 'main' of github.com:porink0424/optuna-dashboard into feat/tslib

This commit is contained in:
porink0424
2024-03-27 13:32:29 +09:00
101 changed files with 19394 additions and 28205 deletions
+5 -2
View File
@@ -1,2 +1,5 @@
venv/**/*.ts
venv/**/*.js
venv
.venv
standalone_app
vscode
tslib
+9 -5
View File
@@ -8,9 +8,9 @@ on:
- '**.py'
- '**.ts'
- '**.tsx'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'optuna_dashboard/package.json'
- 'optuna_dashboard/package-lock.json'
- 'optuna_dashboard/tsconfig.json'
jobs:
test:
runs-on: ubuntu-20.04
@@ -24,8 +24,12 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm install
- run: npm run build:dev
- name: Build bundle.js
working-directory: optuna_dashboard
run: |
npm install
npm run build:dev
- name: Set up Python
uses: actions/setup-python@v2
+25 -9
View File
@@ -5,18 +5,18 @@ on:
- main
paths:
- '.github/workflows/e2e-standalone-tests.yml'
- '**.py'
- '**.ts'
- '**.tsx'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'standalone_app/**.ts'
- 'standalone_app/**.tsx'
- 'standalone_app/package.json'
- 'standalone_app/package-lock.json'
- 'standalone_app/tsconfig.json'
- 'standalone_app/vite.config.js'
jobs:
test:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
strategy:
matrix:
optuna-version: ['git+https://github.com/optuna/optuna.git']
optuna-version: ['optuna']
steps:
- uses: actions/checkout@v3
@@ -31,6 +31,12 @@ jobs:
with:
node-version: '18'
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.10'
architecture: x64
- name: Setup Optuna ${{ matrix.optuna-version }}
run: |
python -m pip install --progress-bar off --upgrade pip setuptools
@@ -41,8 +47,18 @@ jobs:
python -m pip install --progress-bar off .
python -m pip install --progress-bar off pytest-playwright==0.4.3 # next version is flaky
- name: Build rustlib
working-directory: rustlib
run: wasm-pack build --target web
- name: Build tslib
run: make tslib
- name: Build standalone_app
run: make MODE="prd" standalone_app/public/bundle.js
working-directory: standalone_app
run: |
npm install
npx vite build --outDir dist
- name: Install the required browsers
run: playwright install
+9 -9
View File
@@ -30,20 +30,20 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Build rustlib
working-directory: rustlib
run: wasm-pack build --target web
- name: Build tslib
run: make tslib
- name: Build standalone_app
env:
PUBLIC_PATH: /optuna-dashboard/public/
run: make MODE="prd" standalone_app/public/bundle.js
- name: Create publish directory
working-directory: standalone_app
run: |
mkdir gh-publish
cp -r standalone_app/public gh-publish/public
cp standalone_app/favicon.ico gh-publish/favicon.ico
cp standalone_app/github-pages.html gh-publish/index.html
npm install
npx vite build --base=/optuna-dashboard/ --outDir ./gh-pages
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: './gh-publish'
path: './standalone_app/gh-pages'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
+2 -2
View File
@@ -26,8 +26,8 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools
pip install --progress-bar off wheel twine
- run: python setup.py sdist bdist_wheel
pip install --progress-bar off wheel twine build
- run: python -m build --sdist --wheel
- run: twine check dist/*
- name: Create GitHub release
+8 -4
View File
@@ -18,8 +18,12 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm install
- run: npm run build:prd
- name: Build bundle.js
working-directory: optuna_dashboard
run: |
npm install
npm run build:prd
- name: Set up Python
uses: actions/setup-python@v4
@@ -28,8 +32,8 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools
pip install --progress-bar off wheel twine
- run: python setup.py sdist bdist_wheel
pip install --progress-bar off wheel twine build
- run: python -m build --sdist --wheel
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
+45 -7
View File
@@ -6,9 +6,10 @@ on:
- '.eslintrc.js'
- '**.ts'
- '**.tsx'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'optuna_dashboard/package.json'
- 'optuna_dashboard/package-lock.json'
- 'optuna_dashboard/tsconfig.json'
- 'optuna_dashboard/webpack.config.js'
jobs:
lint:
name: Lint checking on Ubuntu
@@ -21,7 +22,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '16'
node-version: '20'
- run: npm install
- run: npm run lint
@@ -37,7 +38,9 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: '16'
- run: |
- name: Build bundle.js
working-directory: optuna_dashboard
run: |
npm install
npm run build:dev
npm run build:prd
@@ -53,7 +56,42 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '16'
- run: |
node-version: '20'
- name: Run jest
working-directory: optuna_dashboard
run: |
npm install
npm run test
test-tslib:
name: Run tests
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@master
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
architecture: x64
- name: Generate test asset
working-directory: tslib/storage/test/
run: |
python -m pip install --progress-bar off --upgrade pip setuptools
pip install --progress-bar off optuna
python generate_assets.py
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '20'
cache: 'npm'
- name: Build test
run: make tslib
- name: Run tslib test
working-directory: tslib/storage
run: |
npm run test
+2 -1
View File
@@ -10,7 +10,7 @@ optuna_dashboard/public/
# JS
node_modules/
standalone_app/public/
pkg/
# VSCode
vscode/assets/
@@ -32,6 +32,7 @@ rustlib/pkg/
.coverage
.coverage.*
coverage.xml
tslib/storage/test/asset/
# Others
.envrc
+3
View File
@@ -26,6 +26,7 @@ The repository is organized as follows:
Node.js v16 is required to compile TypeScript files.
```
$ cd optuna_dashboard/
$ npm install
$ npm run build:dev
```
@@ -34,6 +35,7 @@ $ npm run build:dev
<summary>Watch for files changes</summary>
```
$ cd optuna_dashboard/
$ npm run watch
```
@@ -43,6 +45,7 @@ $ npm run watch
<summary>Production builds</summary>
```
$ cd optuna_dashboard/
$ npm run build:prd
```
+5 -6
View File
@@ -1,12 +1,12 @@
FROM node:20 AS front-builder
WORKDIR /usr/src
WORKDIR /usr/src/optuna_dashboard
ADD ./package.json /usr/src/package.json
ADD ./package-lock.json /usr/src/package-lock.json
ADD ./optuna_dashboard/package.json /usr/src/optuna_dashboard/package.json
ADD ./optuna_dashboard/package-lock.json /usr/src/optuna_dashboard/package-lock.json
RUN npm install
ADD ./tsconfig.json /usr/src/tsconfig.json
ADD ./webpack.config.js /usr/src/webpack.config.js
ADD ./optuna_dashboard/tsconfig.json /usr/src/optuna_dashboard/tsconfig.json
ADD ./optuna_dashboard/webpack.config.js /usr/src/optuna_dashboard/webpack.config.js
ADD ./optuna_dashboard/ts/ /usr/src/optuna_dashboard/ts
RUN mkdir -p /usr/src/optuna_dashboard/public
RUN npm run build:prd
@@ -18,7 +18,6 @@ RUN pip install --upgrade pip setuptools
RUN pip install --progress-bar off PyMySQL[rsa] psycopg2-binary gunicorn optuna-fast-fanova
ADD ./pyproject.toml /usr/src/pyproject.toml
ADD ./setup.py /usr/src/setup.py
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 .
+15 -13
View File
@@ -4,27 +4,28 @@ PYTHON ?= python3
MODE ?= dev
RST_FILES := $(shell find docs -name '*.rst')
PYTHON_FILES := $(shell find optuna_dashboard/ -name '*.py')
DASHBOARD_TS_IN := $(shell find ./optuna_dashboard -name '*.ts' -o -name '*.tsx')
DASHBOARD_TS_SRC := $(shell find ./optuna_dashboard -name '*.ts' -o -name '*.tsx')
DASHBOARD_TS_OUT = optuna_dashboard/public/bundle.js optuna_dashboard/public/favicon.ico
RUSTLIB_OUT = rustlib/pkg/optuna_wasm.js rustlib/pkg/optuna_wasm_bg.wasm rustlib/pkg/package.json
STANDALONE_OUT = standalone_app/public/bundle.js vscode/assets/bundle.js
STANDALONE_SRC := $(shell find ./standalone_app/src -name '*.ts' -o -name '*.tsx')
$(RUSTLIB_OUT): rustlib/src/*.rs rustlib/Cargo.toml
cd rustlib && wasm-pack build --target web
$(STANDALONE_OUT): $(RUSTLIB_OUT)
cd standalone_app && npm install && npm run build:$(MODE)
vscode/assets/bundle.js: $(RUSTLIB_OUT) $(STANDALONE_SRC) tslib
cd standalone_app && npm install && npm run build:vscode
$(DASHBOARD_TS_OUT): $(DASHBOARD_TS_IN)
npm install && npm run build:$(MODE)
$(DASHBOARD_TS_OUT): $(DASHBOARD_TS_SRC)
cd optuna_dashboard && npm install && npm run build:$(MODE)
.PHONY: watch-standalone-app
watch-standalone-app: standalone_app/public/bundle.js
cd standalone_app && npm run watch
.PHONY: tslib
tslib:
cd tslib/types && npm i && npm run build
cd tslib/storage && npm i && npm run build
.PHONY: serve-browser-app
serve-browser-app: standalone_app/public/bundle.js
$(PYTHON) -m http.server 9000 --directory ./standalone_app/
serve-browser-app: tslib $(RUSTLIB_OUT)
cd standalone_app && npm run watch
.PHONY: vscode-extension
vscode-extension: vscode/assets/bundle.js
@@ -32,11 +33,11 @@ vscode-extension: vscode/assets/bundle.js
.PHONY: sdist
sdist: pyproject.toml $(DASHBOARD_TS_OUT)
python setup.py sdist
python -m build --sdist
.PHONY: wheel
wheel: pyproject.toml $(DASHBOARD_TS_OUT)
python setup.py bdist_wheel
python -m build --wheel
.PHONY: docs
docs: docs/conf.py $(RST_FILES)
@@ -50,5 +51,6 @@ fmt:
.PHONY: clean
clean:
rm -rf tslib/types/pkg tslib/storage/pkg
rm -rf optuna_dashboard/public/ doc/_build/
rm -rf rustlib/pkg standalone_app/public/ vscode/assets/ vscode/*.vsix
+5 -3
View File
@@ -5,6 +5,11 @@
[![Read the Docs](https://readthedocs.org/projects/optuna-dashboard/badge/?version=latest)](https://optuna-dashboard.readthedocs.io/en/latest/?badge=latest)
[![Codecov](https://codecov.io/gh/optuna/optuna-dashboard/branch/main/graph/badge.svg)](https://codecov.io/gh/optuna/optuna-dashboard)
:link: [**Website**](https://optuna.org/)
| :page_with_curl: [**Docs**](https://optuna-dashboard.readthedocs.io/en/stable/)
| :gear: [**Install Guide**](https://optuna-dashboard.readthedocs.io/en/stable/getting-started.html#installation)
| :pencil: [**Tutorial**](https://optuna-dashboard.readthedocs.io/en/stable/tutorials/index.html)
| :bulb: [**Examples**](https://github.com/optuna/optuna-examples/tree/main/dashboard)
Real-time dashboard for [Optuna](https://github.com/optuna/optuna).
Code files were originally taken from [Goptuna](https://github.com/c-bata/goptuna).
@@ -112,9 +117,6 @@ or install the code-server extension via [Open VSX](https://open-vsx.org/extensi
Please right-click the SQLite3 files (`*.db` or `*.sqlite3`) in the VS Code file explorer and select the "Open in Optuna Dashboard" command from the dropdown menu.
This extension leverages the browser-only version of Optuna Dashboard, so the same limitations apply.
Please note that we have confirmed the behavior of the code-server extension can be unstable.
When opening SQLite files, the dashboard may not appear on the first attempt, but it should be displayed upon a second try.
## Submitting patches
If you want to contribute, please check [Developers Guide](./CONTRIBUTING.md).
+6 -4
View File
@@ -4,16 +4,18 @@
"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"
"vscode/src/**/*.tsx",
"tslib/**/*.ts",
"tslib/**/*.tsx",
"tslib/**/*.mjs"
],
"ignore": [
"optuna_dashboard/ts/components/PlotlyColorTemplates.ts",
"standalone_app/src/PlotlyDarkMode.ts"
"standalone_app/src/PlotlyDarkMode.ts",
"tslib/**/pkg/*"
]
},
"javascript": {
+1 -4
View File
@@ -48,7 +48,7 @@ Please clone the git repository and execute following commands to build sdist pa
# Node.js v16 is required to compile TypeScript files.
$ npm install
$ npm run build:prd
$ python setup.py sdist
$ python -m build --sdist
Then you can install it like:
@@ -228,9 +228,6 @@ or install the code-server extension via `Open VSX <https://open-vsx.org/extensi
To use, right-click the SQLite3 files (``*.db`` or ``*.sqlite3``) in the file explorer and select the "Open in Optuna Dashboard" from the dropdown menu.
This extension leverages the browser-only version of Optuna Dashboard, so the same limitations apply.
Please note that we have confirmed the behavior of the code-server extension can be unstable.
When opening SQLite files, the dashboard may not appear on the first attempt, but it should be displayed upon a second try.
Google Colaboratory
-------------------
+1 -1
View File
@@ -43,7 +43,7 @@ def make_test_server(
def make_standalone_server(request: pytest.FixtureRequest) -> str:
addr = "127.0.0.1"
port = get_free_port()
directory = "./standalone_app/"
directory = "./standalone_app/dist/"
Handler = http.server.SimpleHTTPRequestHandler
httpd = socketserver.TCPServer(
+28 -26
View File
@@ -1,10 +1,12 @@
import os
import tempfile
import optuna
from playwright.sync_api import Page
import pytest
from ..test_server import make_standalone_server
from ..utils import count_components
@pytest.fixture
@@ -25,47 +27,47 @@ def test_home(
assert title == "Optuna Dashboard (Wasm ver.)"
def create_storage_file(filename: str, study_name: str, storage_type: str):
if storage_type == "rdb":
storage = optuna.storages.RDBStorage(f"sqlite:///{filename}")
elif storage_type == "journal":
storage = optuna.storages.JournalStorage(
optuna.storages.JournalFileStorage(f"{filename}"),
)
else:
assert False, f"Got an unexpected storage_type={storage_type}."
study = optuna.create_study(study_name=study_name, storage=storage)
def objective(trial: optuna.Trial) -> float:
x1 = trial.suggest_float("x1", 0, 10)
x2 = trial.suggest_float("x2", 0, 10)
return (x1 - 2) ** 2 + (x2 - 5) ** 2
study.optimize(objective, n_trials=100)
@pytest.mark.parametrize("storage_type", ["rdb", "journal"])
def test_load_storage(
page: Page,
server_url: str,
storage_type: str,
) -> None:
study_name = "single-objective"
url = f"{server_url}"
def create_storage_file(filename: str):
import optuna
storage = optuna.storages.RDBStorage(f"sqlite:///{filename}")
study = optuna.create_study(study_name=study_name, storage=storage)
def objective(trial: optuna.Trial) -> float:
x1 = trial.suggest_float("x1", 0, 10)
x2 = trial.suggest_float("x2", 0, 10)
return (x1 - 2) ** 2 + (x2 - 5) ** 2
study.optimize(objective, n_trials=100)
with tempfile.TemporaryDirectory() as dir:
with tempfile.NamedTemporaryFile() as fp:
filename = fp.name
path = os.path.join(dir, filename)
create_storage_file(filename)
create_storage_file(filename, study_name, storage_type)
page.goto(url)
with page.expect_file_chooser() as fc_info:
page.get_by_role("button").nth(2).click()
file_chooser = fc_info.value
file_chooser.set_files(path)
page.get_by_role("button").filter(has_text="Storage").click()
file_chooser = fc_info.value
file_chooser.set_files(path)
page.get_by_role("link", name=study_name).click()
def count_components(page: Page, component_name: str):
component_count = page.evaluate(
f"""() => {{
const components = document.querySelectorAll('.{component_name}');
return components.length;
}}"""
)
return component_count
count = count_components(page, "MuiCard-root")
assert count == 4
+11
View File
@@ -1,9 +1,20 @@
from optuna_dashboard._storage import trials_cache
from optuna_dashboard._storage import trials_cache_lock
from optuna_dashboard._storage import trials_last_fetched_at
from playwright.sync_api import Page
def clear_inmemory_cache() -> None:
with trials_cache_lock:
trials_cache.clear()
trials_last_fetched_at.clear()
def count_components(page: Page, component_name: str):
component_count = page.evaluate(
f"""() => {{
const components = document.querySelectorAll('.{component_name}');
return components.length;
}}"""
)
return component_count
-5
View File
@@ -1,5 +0,0 @@
Optuna Dashboard Examples
=========================
Example files have been moved to the [optuna/optuna-examples](https://github.com/optuna/optuna-examples/) repoistory.
You can find the dashboard-related examples in the [dashboard](https://github.com/optuna/optuna-examples/tree/main/dashboard) directory.
+1 -1
View File
@@ -17,4 +17,4 @@ from ._note import save_note # noqa
from ._preference_setting import register_preference_feedback_component # noqa
__version__ = "0.14.0"
__version__ = "0.15.0"
+25 -18
View File
@@ -40,11 +40,11 @@ from ._preferential_history import remove_history
from ._preferential_history import report_history
from ._preferential_history import restore_history
from ._rdb_migration import register_rdb_migration_route
from ._serializer import serialize_frozen_study
from ._serializer import serialize_study_detail
from ._serializer import serialize_study_summary
from ._storage import create_new_study
from ._storage import get_study_summaries
from ._storage import get_study_summary
from ._storage import get_studies
from ._storage import get_study
from ._storage import get_trials
from ._storage_url import get_storage
from .artifact._backend import delete_all_artifacts
@@ -101,9 +101,10 @@ def create_app(
@app.get("/api/studies")
@json_api_view
def list_study_summaries() -> dict[str, Any]:
summaries = get_study_summaries(storage)
serialized = [serialize_study_summary(summary) for summary in summaries]
def list_studies() -> dict[str, Any]:
studies = get_studies(storage)
serialized = [serialize_frozen_study(s) for s in studies]
# TODO(umezawa): Rename `study_summaries` to `studies`.
return {
"study_summaries": serialized,
}
@@ -131,12 +132,12 @@ def create_app(
response.status = 400 # Bad request
return {"reason": f"'{study_name}' already exists"}
summary = get_study_summary(storage, study_id)
if summary is None:
study = get_study(storage, study_id)
if study is None:
response.status = 500 # Internal server error
return {"reason": "Failed to create study"}
response.status = 201 # Created
return {"study_summary": serialize_study_summary(summary)}
return {"study_summary": serialize_frozen_study(study)}
@app.post("/api/studies/<study_id:int>/rename")
@json_api_view
@@ -167,14 +168,14 @@ def create_app(
response.status = 500
storage.delete_study(dst_study._study_id)
return {"reason": str(e)}
new_study_summary = get_study_summary(storage, dst_study._study_id)
if new_study_summary is None:
new_study = get_study(storage, dst_study._study_id)
if new_study is None:
response.status = 500
return {"reason": "Failed to load the new study"}
storage.delete_study(src_study._study_id)
response.status = 201
return serialize_study_summary(new_study_summary)
return serialize_frozen_study(new_study)
@app.delete("/api/studies/<study_id:int>")
@json_api_view
@@ -201,24 +202,24 @@ def create_app(
return {"reason": "`after` should be larger or equal 0."}
except KeyError:
after = 0
summary = get_study_summary(storage, study_id)
if summary is None:
study = get_study(storage, study_id)
if study is None:
response.status = 404 # Not found
return {"reason": f"study_id={study_id} is not found"}
trials = get_trials(storage, study_id)
system_attrs = getattr(summary, "system_attrs", {})
system_attrs = getattr(study, "system_attrs", {})
is_preferential = system_attrs.get(_SYSTEM_ATTR_PREFERENTIAL_STUDY, False)
# TODO(c-bata): Cache best_trials
if is_preferential:
best_trials = get_best_preferential_trials(study_id, storage)
elif len(summary.directions) == 1:
elif len(study.directions) == 1:
if len([t for t in trials if t.state == TrialState.COMPLETE]) == 0:
best_trials = []
else:
best_trials = [storage.get_best_trial(study_id)]
else:
best_trials = get_pareto_front_trials(trials=trials, directions=summary.directions)
best_trials = get_pareto_front_trials(trials=trials, directions=study.directions)
(
# TODO: intersection_search_space and union_search_space look more clear since now we
# have union_user_attrs.
@@ -232,7 +233,7 @@ def create_app(
skipped_trial_ids = get_skipped_trial_ids(system_attrs)
skipped_trial_numbers = [t.number for t in trials if t._trial_id in skipped_trial_ids]
return serialize_study_detail(
summary,
study,
best_trials,
trials[after:],
intersection,
@@ -283,6 +284,12 @@ def create_app(
fig = optuna.visualization.plot_rank(study)
elif plot_type == "edf":
fig = optuna.visualization.plot_edf(study)
elif plot_type == "timeline":
fig = optuna.visualization.plot_timeline(study)
elif plot_type == "param_importances":
fig = optuna.visualization.plot_param_importances(study)
elif plot_type == "pareto_front":
fig = optuna.visualization.plot_pareto_front(study)
else:
response.status = 404 # Not found
return {"reason": f"plot_type={plot_type} is not supported."}
+10
View File
@@ -26,6 +26,12 @@ except Exception as e:
FastFanovaImportanceEvaluator = None # type: ignore
try:
from optuna.importance import PedAnovaImportanceEvaluator # type: ignore[attr-defined]
except ImportError:
PedAnovaImportanceEvaluator = None # type: ignore
if TYPE_CHECKING:
from typing import Callable
from typing import Optional
@@ -64,6 +70,10 @@ def _get_param_importances(
*,
target: Optional[Callable[[FrozenTrial], float]] = None,
) -> dict[str, float]:
if PedAnovaImportanceEvaluator is not None:
# TODO(nabenabe0928): We might want to pass baseline_quantile as an argument in the future.
return get_param_importances(study, target=target, evaluator=PedAnovaImportanceEvaluator())
if FastFanovaImportanceEvaluator is not None:
try:
evaluator = FastFanovaImportanceEvaluator(completed_trials=completed_trials)
+14 -21
View File
@@ -12,7 +12,7 @@ from optuna.distributions import BaseDistribution
from optuna.distributions import CategoricalDistribution
from optuna.distributions import FloatDistribution
from optuna.distributions import IntDistribution
from optuna.study import StudySummary
from optuna.study._frozen import FrozenStudy
from optuna.trial import FrozenTrial
from . import _note as note
@@ -116,25 +116,20 @@ def serialize_attrs(attrs: dict[str, Any]) -> list[Attribute]:
return serialized
def serialize_study_summary(summary: StudySummary) -> dict[str, Any]:
def serialize_frozen_study(study: FrozenStudy) -> dict[str, Any]:
serialized = {
"study_id": summary._study_id,
"study_name": summary.study_name,
"directions": [d.name.lower() for d in summary.directions],
"user_attrs": serialize_attrs(summary.user_attrs),
"is_preferential": getattr(summary, "_system_attrs", {}).get(
_SYSTEM_ATTR_PREFERENTIAL_STUDY, False
),
"study_id": study._study_id,
"study_name": study.study_name,
"directions": [d.name.lower() for d in study.directions],
"user_attrs": serialize_attrs(study.user_attrs),
"is_preferential": study.system_attrs.get(_SYSTEM_ATTR_PREFERENTIAL_STUDY, False),
}
if summary.datetime_start is not None:
serialized["datetime_start"] = summary.datetime_start.isoformat()
return serialized
def serialize_study_detail(
summary: StudySummary,
study: FrozenStudy,
best_trials: list[FrozenTrial],
trials: list[FrozenTrial],
intersection: list[tuple[str, BaseDistribution]],
@@ -145,20 +140,18 @@ def serialize_study_detail(
skipped_trial_numbers: list[int],
) -> dict[str, Any]:
serialized: dict[str, Any] = {
"name": summary.study_name,
"directions": [d.name.lower() for d in summary.directions],
"user_attrs": serialize_attrs(summary.user_attrs),
"name": study.study_name,
"directions": [d.name.lower() for d in study.directions],
"user_attrs": serialize_attrs(study.user_attrs),
}
system_attrs = getattr(summary, "system_attrs", {})
system_attrs = study.system_attrs
serialized["artifacts"] = list_study_artifacts(system_attrs)
if summary.datetime_start is not None:
serialized["datetime_start"] = summary.datetime_start.isoformat()
serialized["trials"] = [
serialize_frozen_trial(summary._study_id, trial, system_attrs) for trial in trials
serialize_frozen_trial(study._study_id, trial, system_attrs) for trial in trials
]
serialized["best_trials"] = [
serialize_frozen_trial(summary._study_id, trial, system_attrs) for trial in best_trials
serialize_frozen_trial(study._study_id, trial, system_attrs) for trial in best_trials
]
serialized["intersection_search_space"] = serialize_search_space(intersection)
serialized["union_search_space"] = serialize_search_space(union)
+8 -28
View File
@@ -3,19 +3,14 @@ from __future__ import annotations
from datetime import datetime
from datetime import timedelta
import threading
import typing
from optuna.storages import BaseStorage
from optuna.storages import RDBStorage
from optuna.study import StudyDirection
from optuna.study import StudySummary
from optuna.study._frozen import FrozenStudy
from optuna.trial import FrozenTrial
if typing.TYPE_CHECKING:
from optuna.study._frozen import FrozenStudy
# In-memory trials cache
trials_cache_lock = threading.Lock()
trials_cache: dict[int, list[FrozenTrial]] = {}
@@ -49,19 +44,19 @@ def get_trials(storage: BaseStorage, study_id: int) -> list[FrozenTrial]:
return trials
def get_study_summaries(storage: BaseStorage) -> list[StudySummary]:
def get_studies(storage: BaseStorage) -> list[FrozenStudy]:
frozen_studies = storage.get_all_studies()
if isinstance(storage, RDBStorage):
frozen_studies = sorted(frozen_studies, key=lambda s: s._study_id)
return [_frozen_study_to_study_summary(s) for s in frozen_studies]
return frozen_studies
def get_study_summary(storage: BaseStorage, study_id: int) -> StudySummary | None:
summaries = get_study_summaries(storage)
for summary in summaries:
if summary._study_id != study_id:
def get_study(storage: BaseStorage, study_id: int) -> FrozenStudy | None:
studies = get_studies(storage)
for s in studies:
if s._study_id != study_id:
continue
return summary
return s
return None
@@ -70,18 +65,3 @@ def create_new_study(
) -> int:
study_id = storage.create_new_study(directions, study_name=study_name)
return study_id
def _frozen_study_to_study_summary(frozen_study: "FrozenStudy") -> StudySummary:
is_single = len(frozen_study.directions) == 1
return StudySummary(
study_name=frozen_study.study_name,
study_id=frozen_study._study_id,
direction=frozen_study.direction if is_single else None,
directions=frozen_study.directions if not is_single else None,
user_attrs=frozen_study.user_attrs,
system_attrs=frozen_study.system_attrs,
best_trial=None,
n_trials=-1, # This field isn't used by Dashboard.
datetime_start=None,
)
+15
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import os.path
from pathlib import Path
import re
from typing import TYPE_CHECKING
@@ -58,11 +59,25 @@ def get_storage(
return guess_storage_from_url(storage)
def _has_sqlite_header(storage_url: str) -> bool:
storage_path = Path(storage_url)
SQLITE_HEADER = (
b"SQLite format 3\x00" # see https://github.com/optuna/optuna-dashboard/pull/800
)
with storage_path.open(mode="rb") as f:
header = f.read(len(SQLITE_HEADER))
return header == SQLITE_HEADER
def guess_storage_from_url(storage_url: str) -> BaseStorage:
if storage_url.startswith("redis"):
return get_journal_redis_storage(storage_url)
if os.path.isfile(storage_url):
if _has_sqlite_header(storage_url):
raise ValueError(
f"Please specify 'sqlite:///{storage_url}' to use SQLite3 (RDBStorage)"
)
return get_journal_file_storage(storage_url)
if rfc1738_pattern.match(storage_url) is not None:
+14480
View File
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
{
"name": "optuna-dashboard",
"private": true,
"version": "0.0.1",
"description": "Dashboard for Optuna",
"main": "index.js",
"scripts": {
"watch": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack --watch",
"build": "webpack",
"build:dev": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack",
"build:prd": "NODE_ENV=production webpack",
"test": "jest ts/tests"
},
"author": "Masashi Shibata",
"license": "MIT",
"dependencies": {
"@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",
"@tanstack/react-virtual": "^3.1.2",
"@tanstack/react-query": "^5.18.1",
"@types/three": "^0.160.0",
"axios": "^1.6.7",
"elkjs": "^0.9.1",
"notistack": "^3.0.1",
"plotly.js-dist-min": "^2.28.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.21.3",
"react-syntax-highlighter": "^15.5.0",
"reactflow": "^11.10.3",
"recoil": "^0.7.7",
"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.23.9",
"@babel/preset-env": "^7.23.9",
"@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",
"compression-webpack-plugin": "^11.0.0",
"css-loader": "^6.9.1",
"esbuild-loader": "^4.0.3",
"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"
}
}
+10 -30
View File
@@ -3,7 +3,6 @@ import { useSnackbar } from "notistack"
import {
getStudyDetailAPI,
getStudySummariesAPI,
getParamImportances,
createNewStudyAPI,
deleteStudyAPI,
saveStudyNoteAPI,
@@ -25,10 +24,10 @@ import {
import {
studyDetailsState,
studySummariesState,
paramImportanceState,
isFileUploading,
artifactIsAvailable,
plotlypyIsAvailableState,
studyDetailLoadingState,
reloadIntervalState,
trialsUpdatingState,
studySummariesLoadingState,
@@ -43,8 +42,6 @@ export const actionCreator = () => {
const [studyDetails, setStudyDetails] =
useRecoilState<StudyDetails>(studyDetailsState)
const setReloadInterval = useSetRecoilState<number>(reloadIntervalState)
const [paramImportance, setParamImportance] =
useRecoilState<StudyParamImportance>(paramImportanceState)
const setUploading = useSetRecoilState<boolean>(isFileUploading)
const setTrialsUpdating = useSetRecoilState(trialsUpdatingState)
const setArtifactIsAvailable = useSetRecoilState<boolean>(artifactIsAvailable)
@@ -54,6 +51,9 @@ export const actionCreator = () => {
const setStudySummariesLoading = useSetRecoilState<boolean>(
studySummariesLoadingState
)
const [studyDetailLoading, setStudyDetailLoading] = useRecoilState<
Record<number, boolean>
>(studyDetailLoadingState)
const setStudyDetailState = (studyId: number, study: StudyDetail) => {
setStudyDetails((prevVal) => {
@@ -207,15 +207,6 @@ export const actionCreator = () => {
setStudyDetailState(studyId, newStudy)
}
const setStudyParamImportanceState = (
studyId: number,
importance: ParamImportance[][]
) => {
const newVal = Object.assign({}, paramImportance)
newVal[studyId] = importance
setParamImportance(newVal)
}
const updateAPIMeta = () => {
getMetaInfoAPI().then((r) => {
setArtifactIsAvailable(r.artifact_is_available)
@@ -244,6 +235,10 @@ export const actionCreator = () => {
}
const updateStudyDetail = (studyId: number) => {
if (studyDetailLoading[studyId]) {
return
}
setStudyDetailLoading({ ...studyDetailLoading, [studyId]: true })
let nLocalFixedTrials = 0
if (studyId in studyDetails) {
const currentTrials = studyDetails[studyId].trials
@@ -255,6 +250,7 @@ export const actionCreator = () => {
}
getStudyDetailAPI(studyId, nLocalFixedTrials)
.then((study) => {
setStudyDetailLoading({ ...studyDetailLoading, [studyId]: false })
const currentFixedTrials =
studyId in studyDetails
? studyDetails[studyId].trials.slice(0, nLocalFixedTrials)
@@ -263,6 +259,7 @@ export const actionCreator = () => {
setStudyDetailState(studyId, study)
})
.catch((err) => {
setStudyDetailLoading({ ...studyDetailLoading, [studyId]: false })
const reason = err.response?.data.reason
if (reason !== undefined) {
enqueueSnackbar(`Failed to fetch study (reason=${reason})`, {
@@ -273,22 +270,6 @@ export const actionCreator = () => {
})
}
const updateParamImportance = (studyId: number) => {
getParamImportances(studyId)
.then((importance) => {
setStudyParamImportanceState(studyId, importance)
})
.catch((err) => {
const reason = err.response?.data.reason
enqueueSnackbar(
`Failed to load hyperparameter importance (reason=${reason})`,
{
variant: "error",
}
)
})
}
const createNewStudy = (studyName: string, directions: StudyDirection[]) => {
createNewStudyAPI(studyName, directions)
.then((study_summary) => {
@@ -714,7 +695,6 @@ export const actionCreator = () => {
updateAPIMeta,
updateStudyDetail,
updateStudySummaries,
updateParamImportance,
createNewStudy,
deleteStudy,
renameStudy,
+3
View File
@@ -452,6 +452,9 @@ export enum PlotType {
ParallelCoordinate = "parallel_coordinate",
Rank = "rank",
EDF = "edf",
Timeline = "timeline",
ParamImportances = "param_importances",
ParetoFront = "pareto_front",
}
export const getPlotAPI = (
studyId: number,
+106 -90
View File
@@ -15,6 +15,18 @@ import {
import { CompareStudies } from "./CompareStudies"
import { StudyDetail } from "./StudyDetail"
import { StudyList } from "./StudyList"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
},
},
})
export const App: FC = () => {
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)")
@@ -38,95 +50,99 @@ export const App: FC = () => {
}
return (
<RecoilRoot>
<ThemeProvider theme={theme}>
<CssBaseline />
<Box
sx={{
backgroundColor: colorMode === "dark" ? "#121212" : "#ffffff",
width: "100%",
minHeight: "100vh",
}}
>
<SnackbarProvider maxSnack={3}>
<Router>
<Routes>
<Route
path={URL_PREFIX + "/studies/:studyId/analytics"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"analytics"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/trials"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"trialList"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/trialTable"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"trialTable"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/note"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"note"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/graph"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"graph"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"top"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/preference-history"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"preferenceHistory"}
/>
}
/>
<Route
path={URL_PREFIX + "/compare-studies"}
element={<CompareStudies toggleColorMode={toggleColorMode} />}
/>
<Route
path={URL_PREFIX + "/"}
element={<StudyList toggleColorMode={toggleColorMode} />}
/>
</Routes>
</Router>
</SnackbarProvider>
</Box>
</ThemeProvider>
</RecoilRoot>
<QueryClientProvider client={queryClient}>
<RecoilRoot>
<ThemeProvider theme={theme}>
<CssBaseline />
<Box
sx={{
backgroundColor: colorMode === "dark" ? "#121212" : "#ffffff",
width: "100%",
minHeight: "100vh",
}}
>
<SnackbarProvider maxSnack={3}>
<Router>
<Routes>
<Route
path={URL_PREFIX + "/studies/:studyId/analytics"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"analytics"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/trials"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"trialList"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/trialTable"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"trialTable"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/note"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"note"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/graph"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"graph"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"top"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/preference-history"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"preferenceHistory"}
/>
}
/>
<Route
path={URL_PREFIX + "/compare-studies"}
element={
<CompareStudies toggleColorMode={toggleColorMode} />
}
/>
<Route
path={URL_PREFIX + "/"}
element={<StudyList toggleColorMode={toggleColorMode} />}
/>
</Routes>
</Router>
</SnackbarProvider>
</Box>
</ThemeProvider>
</RecoilRoot>
</QueryClientProvider>
)
}
+7 -6
View File
@@ -361,15 +361,16 @@ export const AppDrawer: FC<{
<Box
sx={{
position: "absolute",
top: "10%",
left: "10%",
overflow: "auto",
width: "80%",
height: "80%",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
overflow: "scroll",
width: "500px",
height: "400px",
bgcolor: "background.paper",
}}
>
<Settings />
<Settings handleClose={handleSettingClose} />
</Box>
</Modal>
</ListItem>
@@ -198,7 +198,7 @@ function loadRhino3dm(
handleLoadedGeometries: (geometries: THREE.BufferGeometry[]) => THREE.Box3
) {
const rhino3dmLoader = new Rhino3dmLoader()
rhino3dmLoader.setLibraryPath("https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/")
rhino3dmLoader.setLibraryPath("https://cdn.jsdelivr.net/npm/rhino3dm@8.4.0/")
rhino3dmLoader.load(props.src, (object: THREE.Object3D) => {
const meshes = object.children as THREE.Mesh[]
const rhinoGeometries = meshes.map((mesh) => mesh.geometry)
+2 -2
View File
@@ -35,7 +35,7 @@ interface DataGridColumn<T> {
label: string
sortable?: boolean
less?: (a: T, b: T, ascending: boolean) => number
filterChoices?: string[]
filterChoices?: (string | null)[]
toCellValue?: (rowIndex: number) => string | React.ReactNode
padding?: "normal" | "checkbox" | "none"
}
@@ -333,7 +333,7 @@ function DataGridHeaderColumn<T>(props: {
<CheckBoxOutlineBlankIcon color="primary" />
)}
</ListItemIcon>
{choice}
{choice ?? "(missing value)"}
</MenuItem>
))}
</Menu>
@@ -1,6 +1,7 @@
import React, { FC, useEffect } from "react"
import { TextField, TextFieldProps } from "@mui/material"
// TODO(c-bata): Remove this and use `useDeferredValue` instead.
export const DebouncedInputTextField: FC<{
onChange: (s: string, valid: boolean) => void
delay: number
+17 -11
View File
@@ -15,8 +15,9 @@ import blue from "@mui/material/colors/blue"
import { useMergedUnionSearchSpace } from "../searchSpace"
import { usePlotlyColorTheme } from "../state"
import { getAxisInfo } from "../graphUtil"
import { getPlotAPI, PlotType } from "../apiClient"
import { PlotType } from "../apiClient"
import { useBackendRender } from "../state"
import { usePlot } from "../hooks/usePlot"
const plotDomId = "graph-contour"
@@ -36,18 +37,23 @@ const ContourBackend: FC<{
const studyId = study?.id
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
const { data, layout, error } = usePlot({
numCompletedTrials,
studyId,
plotType: PlotType.Contour,
})
useEffect(() => {
if (studyId === undefined) {
return
if (data && layout) {
plotly.react(plotDomId, data, layout)
}
getPlotAPI(studyId, PlotType.Contour)
.then(({ data, layout }) => {
plotly.react(plotDomId, data, layout)
})
.catch((err) => {
console.error(err)
})
}, [studyId, numCompletedTrials])
}, [data, layout])
useEffect(() => {
if (error) {
console.error(error)
}
}, [error])
return <Box id={plotDomId} sx={{ height: "450px" }} />
}
@@ -2,9 +2,14 @@ import * as plotly from "plotly.js-dist-min"
import React, { FC, useEffect } from "react"
import { Typography, useTheme, Box, Card, CardContent } from "@mui/material"
import { actionCreator } from "../action"
import { useParamImportanceValue, useStudyDirections } from "../state"
import { usePlotlyColorTheme } from "../state"
import { useParamImportance } from "../hooks/useParamImportance"
import {
useStudyDirections,
usePlotlyColorTheme,
useBackendRender,
} from "../state"
import { PlotType } from "../apiClient"
import { usePlot } from "../hooks/usePlot"
const plotDomId = "graph-hyperparameter-importances"
@@ -12,14 +17,67 @@ export const GraphHyperparameterImportance: FC<{
studyId: number
study: StudyDetail | null
graphHeight: string
}> = ({ studyId, study = null, graphHeight }) => {
if (useBackendRender()) {
return (
<GraphHyperparameterImportanceBackend
studyId={studyId}
study={study}
graphHeight={graphHeight}
/>
)
} else {
return (
<GraphHyperparameterImportanceFrontend
studyId={studyId}
study={study}
graphHeight={graphHeight}
/>
)
}
}
const GraphHyperparameterImportanceBackend: FC<{
studyId: number
study: StudyDetail | null
graphHeight: string
}> = ({ studyId, study = null, graphHeight }) => {
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
const { data, layout, error } = usePlot({
numCompletedTrials,
studyId,
plotType: PlotType.ParamImportances,
})
useEffect(() => {
if (data && layout) {
plotly.react(plotDomId, data, layout)
}
}, [data, layout])
useEffect(() => {
if (error) {
console.error(error)
}
}, [error])
return <Box id={plotDomId} sx={{ height: graphHeight }} />
}
const GraphHyperparameterImportanceFrontend: FC<{
studyId: number
study: StudyDetail | null
graphHeight: string
}> = ({ studyId, study = null, graphHeight }) => {
const theme = useTheme()
const colorTheme = usePlotlyColorTheme(theme.palette.mode)
const action = actionCreator()
const importances = useParamImportanceValue(studyId)
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
const { importances } = useParamImportance({
numCompletedTrials,
studyId,
})
const nObjectives = useStudyDirections(studyId)?.length
const objectiveNames: string[] =
study?.objective_names ||
@@ -27,11 +85,7 @@ export const GraphHyperparameterImportance: FC<{
[]
useEffect(() => {
action.updateParamImportance(studyId)
}, [numCompletedTrials])
useEffect(() => {
if (importances !== null && nObjectives === importances.length) {
if (importances !== undefined && nObjectives === importances.length) {
plotParamImportance(importances, objectiveNames, colorTheme)
}
}, [nObjectives, importances, colorTheme])
@@ -17,8 +17,9 @@ import {
useParamTargets,
} from "../trialFilter"
import { useMergedUnionSearchSpace } from "../searchSpace"
import { getPlotAPI, PlotType } from "../apiClient"
import { PlotType } from "../apiClient"
import { useBackendRender } from "../state"
import { usePlot } from "../hooks/usePlot"
const plotDomId = "graph-parallel-coordinate"
@@ -102,18 +103,24 @@ const GraphParallelCoordinateBackend: FC<{
const studyId = study?.id
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
const { data, layout, error } = usePlot({
numCompletedTrials,
studyId,
plotType: PlotType.ParallelCoordinate,
})
useEffect(() => {
if (studyId === undefined) {
return
if (data && layout) {
plotly.react(plotDomId, data, layout)
}
getPlotAPI(studyId, PlotType.ParallelCoordinate)
.then(({ data, layout }) => {
plotly.react(plotDomId, data, layout)
})
.catch((err) => {
console.error(err)
})
}, [studyId, numCompletedTrials])
}, [data, layout])
useEffect(() => {
if (error) {
console.error(error)
}
}, [error])
return <Box id={plotDomId} sx={{ height: "450px" }} />
}
@@ -14,11 +14,50 @@ import {
import { makeHovertext } from "../graphUtil"
import { usePlotlyColorTheme } from "../state"
import { useNavigate } from "react-router-dom"
import { PlotType } from "../apiClient"
import { useBackendRender } from "../state"
import { usePlot } from "../hooks/usePlot"
const plotDomId = "graph-pareto-front"
export const GraphParetoFront: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
if (useBackendRender()) {
return <GraphParetoFrontBackend study={study} />
} else {
return <GraphParetoFrontFrontend study={study} />
}
}
const GraphParetoFrontBackend: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const studyId = study?.id
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
const { data, layout, error } = usePlot({
numCompletedTrials,
studyId,
plotType: PlotType.ParetoFront,
})
useEffect(() => {
if (data && layout) {
plotly.react(plotDomId, data, layout)
}
}, [data, layout])
useEffect(() => {
if (error) {
console.error(error)
}
}, [error])
return <Box id={plotDomId} sx={{ height: "450px" }} />
}
const GraphParetoFrontFrontend: FC<{
study: StudyDetail | null
}> = ({ study = null }) => {
const theme = useTheme()
const colorTheme = usePlotlyColorTheme(theme.palette.mode)
+18 -11
View File
@@ -13,8 +13,9 @@ import {
} from "@mui/material"
import { getAxisInfo, makeHovertext } from "../graphUtil"
import { useMergedUnionSearchSpace } from "../searchSpace"
import { PlotType } from "../apiClient"
import { usePlotlyColorTheme, useBackendRender } from "../state"
import { getPlotAPI, PlotType } from "../apiClient"
import { usePlot } from "../hooks/usePlot"
const plotDomId = "graph-rank"
@@ -46,18 +47,24 @@ const GraphRankBackend: FC<{
const studyId = study?.id
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
const { data, layout, error } = usePlot({
numCompletedTrials,
studyId,
plotType: PlotType.Rank,
})
useEffect(() => {
if (studyId === undefined) {
return
if (data && layout) {
plotly.react(plotDomId, data, layout)
}
getPlotAPI(studyId, PlotType.Rank)
.then(({ data, layout }) => {
plotly.react(plotDomId, data, layout)
})
.catch((err) => {
console.error(err)
})
}, [studyId, numCompletedTrials])
}, [data, layout])
useEffect(() => {
if (error) {
console.error(error)
}
}, [error])
return <Box id={plotDomId} sx={{ height: "450px" }} />
}
+18 -11
View File
@@ -19,8 +19,9 @@ import {
useParamTargets,
} from "../trialFilter"
import { useMergedUnionSearchSpace } from "../searchSpace"
import { PlotType } from "../apiClient"
import { usePlotlyColorTheme, useBackendRender } from "../state"
import { getPlotAPI, PlotType } from "../apiClient"
import { usePlot } from "../hooks/usePlot"
const plotDomId = "graph-slice"
@@ -47,18 +48,24 @@ const GraphSliceBackend: FC<{
const studyId = study?.id
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
const { data, layout, error } = usePlot({
numCompletedTrials,
studyId,
plotType: PlotType.Slice,
})
useEffect(() => {
if (studyId === undefined) {
return
if (data && layout) {
plotly.react(plotDomId, data, layout)
}
getPlotAPI(studyId, PlotType.Slice)
.then(({ data, layout }) => {
plotly.react(plotDomId, data, layout)
})
.catch((err) => {
console.error(err)
})
}, [studyId, numCompletedTrials])
}, [data, layout])
useEffect(() => {
if (error) {
console.error(error)
}
}, [error])
return <Box id={plotDomId} sx={{ height: "450px" }} />
}
@@ -3,12 +3,55 @@ import React, { FC, useEffect } from "react"
import { Card, CardContent, Grid, Typography, useTheme } from "@mui/material"
import { makeHovertext } from "../graphUtil"
import { usePlotlyColorTheme } from "../state"
import { PlotType } from "../apiClient"
import { useBackendRender } from "../state"
import { usePlot } from "../hooks/usePlot"
const plotDomId = "graph-timeline"
const maxBars = 100
export const GraphTimeline: FC<{
study: StudyDetail | null
}> = ({ study }) => {
if (useBackendRender()) {
return <GraphTimelineBackend study={study} />
} else {
return <GraphTimelineFrontend study={study} />
}
}
const GraphTimelineBackend: FC<{
study: StudyDetail | null
}> = ({ study }) => {
const studyId = study?.id
const numCompletedTrials =
study?.trials.filter((t) => t.state === "Complete").length || 0
const { data, layout, error } = usePlot({
numCompletedTrials,
studyId,
plotType: PlotType.Timeline,
})
useEffect(() => {
if (data && layout) {
plotly.react(plotDomId, data, layout)
}
}, [data, layout])
useEffect(() => {
if (error) {
console.error(error)
}
}, [error])
return (
<Grid item xs={9}>
<div id={plotDomId} />
</Grid>
)
}
const GraphTimelineFrontend: FC<{
study: StudyDetail | null
}> = ({ study }) => {
const theme = useTheme()
const colorTheme = usePlotlyColorTheme(theme.palette.mode)
+125 -51
View File
@@ -1,73 +1,147 @@
import React, { FC, useState } from "react"
import React from "react"
import {
Typography,
Select,
Switch,
MenuItem,
Grid,
SelectChangeEvent,
Stack,
useTheme,
IconButton,
Box,
} from "@mui/material"
import ClearIcon from "@mui/icons-material/Clear"
import { useRecoilState } from "recoil"
import { plotlyColorThemeState, plotBackendRenderingState } from "../state"
import { useRecoilValue, useSetRecoilState } from "recoil"
import { plotlyColorTheme } from "../state"
interface SettingsProps {
handleClose: () => void
}
export const Settings: FC = () => {
const colorTheme = useRecoilValue<PlotlyColorTheme>(plotlyColorTheme)
const setPlotlyColorTheme = useSetRecoilState(plotlyColorTheme)
const [darkModeColor, setDarkModeColor] = useState(colorTheme.dark)
const [lightModeColor, setLightModeColor] = useState(colorTheme.light)
export const Settings = ({ handleClose }: SettingsProps) => {
const theme = useTheme()
const [plotlyColorTheme, setPlotlyColorTheme] = useRecoilState(
plotlyColorThemeState
)
const [plotBackendRendering, setPlotBackendRendering] = useRecoilState(
plotBackendRenderingState
)
const handleDarkModeColorChange = (event: SelectChangeEvent) => {
setDarkModeColor(event.target.value)
setPlotlyColorTheme({ dark: event.target.value, light: lightModeColor })
const dark = event.target.value as PlotlyColorThemeDark
setPlotlyColorTheme((cur) => ({ ...cur, dark }))
}
const handleLightModeColorChange = (event: SelectChangeEvent) => {
setLightModeColor(event.target.value)
setPlotlyColorTheme({ dark: darkModeColor, light: event.target.value })
const light = event.target.value as PlotlyColorThemeLight
setPlotlyColorTheme((cur) => ({ ...cur, light }))
}
const togglePlotBackendRendering = () => {
setPlotBackendRendering((cur) => !cur)
}
return (
<Grid container spacing={4} sx={{ padding: "40px" }}>
<Grid item xs={12}>
<Typography variant="h3" gutterBottom color="textSecondary">
<Box component="div" sx={{ position: "relative" }}>
<Stack
spacing={4}
sx={{
p: "2rem",
}}
>
<Typography
variant="h4"
sx={{ fontWeight: theme.typography.fontWeightBold }}
>
Settings
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h5" gutterBottom color="textPrimary">
Plotly Color Scales
</Typography>
</Grid>
<Grid item xs={2}>
<Typography variant="h6" color="textSecondary">
Dark Mode
</Typography>
</Grid>
<Grid item xs={10} sx={{ display: "flex", alignItems: "center" }}>
<Select value={darkModeColor} onChange={handleDarkModeColorChange}>
<MenuItem value={"default"}>Default</MenuItem>
<MenuItem value={"seaborn"}>Seaborn</MenuItem>
<MenuItem value={"presentation"}>Presentation</MenuItem>
<MenuItem value={"ggplot2"}>GGPlot2</MenuItem>
</Select>
</Grid>
<Stack spacing={2}>
<Typography
variant="h5"
sx={{ fontWeight: theme.typography.fontWeightBold }}
>
Plotly Color Scales
</Typography>
{theme.palette.mode === "dark" ? (
<>
<Stack direction="row" spacing={2} alignItems="center">
<Typography variant="h6">Dark Mode</Typography>
<Select
disabled
value={plotlyColorTheme.dark}
onChange={handleDarkModeColorChange}
>
{(
[{ value: "default", label: "Default" }] as {
value: PlotlyColorThemeDark
label: string
}[]
).map((v) => (
<MenuItem key={v.value} value={v.value}>
{v.label}
</MenuItem>
))}
</Select>
</Stack>
<Typography color="textSecondary">
Only the "Default" color scale is supported in dark mode
</Typography>
</>
) : (
<Stack direction="row" spacing={2} alignItems="center">
<Typography variant="h6">Light Mode</Typography>
<Select
value={plotlyColorTheme.light}
onChange={handleLightModeColorChange}
>
{(
[
{ value: "default", label: "Default" },
{ value: "seaborn", label: "Seaborn" },
{ value: "presentation", label: "Presentation" },
{ value: "ggplot2", label: "GGPlot2" },
] as {
value: PlotlyColorThemeLight
label: string
}[]
).map((v) => (
<MenuItem key={v.value} value={v.value}>
{v.label}
</MenuItem>
))}
</Select>
</Stack>
)}
</Stack>
<Grid item xs={2}>
<Typography variant="h6" color="textSecondary">
Light Mode
</Typography>
</Grid>
<Grid item xs={10} sx={{ display: "flex", alignItems: "center" }}>
<Select value={lightModeColor} onChange={handleLightModeColorChange}>
<MenuItem value={"default"}>Default</MenuItem>
<MenuItem value={"seaborn"}>Seaborn</MenuItem>
<MenuItem value={"presentation"}>Presentation</MenuItem>
<MenuItem value={"ggplot2"}>GGPlot2</MenuItem>
</Select>
</Grid>
</Grid>
<Stack>
<Typography
variant="h5"
sx={{ fontWeight: theme.typography.fontWeightBold }}
>
Use Plotlypy
</Typography>
<Switch
checked={plotBackendRendering}
onChange={togglePlotBackendRendering}
value="enable"
/>
</Stack>
</Stack>
<IconButton
sx={{
position: "absolute",
top: "1rem",
right: "1rem",
width: "2rem",
height: "2rem",
}}
onClick={handleClose}
>
<ClearIcon />
</IconButton>
</Box>
)
}
+33 -28
View File
@@ -1,4 +1,10 @@
import React, { FC, useEffect, useState } from "react"
import React, {
FC,
useEffect,
useState,
useDeferredValue,
useMemo,
} from "react"
import { useNavigate } from "react-router-dom"
import { useRecoilValue } from "recoil"
import { Link } from "react-router-dom"
@@ -26,7 +32,6 @@ import CompareIcon from "@mui/icons-material/Compare"
import DriveFileRenameOutlineIcon from "@mui/icons-material/DriveFileRenameOutline"
import { actionCreator } from "../action"
import { DebouncedInputTextField } from "./Debounce"
import { studySummariesLoadingState, studySummariesState } from "../state"
import { styled } from "@mui/system"
import { AppDrawer } from "./AppDrawer"
@@ -41,7 +46,8 @@ export const StudyList: FC<{
const theme = useTheme()
const action = actionCreator()
const [studyFilterText, setStudyFilterText] = React.useState<string>("")
const [_studyFilterText, setStudyFilterText] = React.useState<string>("")
const studyFilterText = useDeferredValue(_studyFilterText)
const studyFilter = (row: StudySummary) => {
const keywords = studyFilterText.split(" ")
return !keywords.every((k) => {
@@ -64,12 +70,14 @@ export const StudyList: FC<{
const query = useQuery()
const initialSortBy = query.get("studies_order_by") === "asc" ? "asc" : "desc"
const [sortBy, setSortBy] = useState<"asc" | "desc">(initialSortBy)
const filteredStudies = useMemo(() => {
let filteredStudies: StudySummary[] = studies.filter((s) => !studyFilter(s))
if (sortBy === "desc") {
filteredStudies = filteredStudies.reverse()
}
return filteredStudies
}, [studyFilterText, studies, sortBy])
let filteredStudies = studies.filter((s) => !studyFilter(s))
if (sortBy === "desc") {
filteredStudies = filteredStudies.reverse()
}
useEffect(() => {
action.updateStudySummaries()
}, [])
@@ -144,7 +152,7 @@ export const StudyList: FC<{
to={`${URL_PREFIX}/studies/${study.study_id}`}
>
<CardContent>
<Typography variant="h5">
<Typography variant="h5" sx={{ wordBreak: "break-all" }}>
{study.study_id}. {study.study_name}
</Typography>
<Typography
@@ -201,26 +209,23 @@ export const StudyList: FC<{
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<Box sx={{ display: "flex" }}>
<DebouncedInputTextField
onChange={(s) => {
setStudyFilterText(s)
<TextField
onChange={(e) => {
setStudyFilterText(e.target.value)
}}
delay={500}
textFieldProps={{
fullWidth: true,
id: "search-study",
variant: "outlined",
placeholder: "Search study",
sx: { maxWidth: 500 },
InputProps: {
startAdornment: (
<InputAdornment position="start">
<SvgIcon fontSize="small" color="action">
<Search />
</SvgIcon>
</InputAdornment>
),
},
id="search-study"
variant="outlined"
placeholder="Search study"
fullWidth
sx={{ maxWidth: 500 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SvgIcon fontSize="small" color="action">
<Search />
</SvgIcon>
</InputAdornment>
),
}}
/>
{sortBySelect}
+87 -50
View File
@@ -30,6 +30,8 @@ import { TrialFormWidgets } from "./TrialFormWidgets"
import { TrialArtifactCards } from "./Artifact/TrialArtifactCards"
import { useQuery } from "../urlQuery"
import { useVirtualizer } from "@tanstack/react-virtual"
const states: TrialState[] = [
"Complete",
"Pruned",
@@ -339,6 +341,14 @@ export const TrialList: FC<{ studyDetail: StudyDetail | null }> = ({
(state) => allTrials.filter((t) => t.state === state).length
)
}, [studyDetail?.trials])
const listParentRef = React.useRef(null)
const rowVirtualizer = useVirtualizer({
count: trials.length,
getScrollElement: () => listParentRef.current,
estimateSize: () => 73.31,
overscan: 10,
})
const trialListWidth = 200
@@ -348,13 +358,14 @@ export const TrialList: FC<{ studyDetail: StudyDetail | null }> = ({
return (
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}>
<Box
ref={listParentRef}
sx={{
minWidth: trialListWidth,
overflow: "auto",
height: `calc(100vh - ${theme.spacing(8)})`,
}}
>
<List>
<List sx={{ position: "relative" }}>
<ListSubheader sx={{ display: "flex", flexDirection: "row" }}>
<Typography sx={{ p: theme.spacing(1, 0) }}>
{trials.length} Trials
@@ -412,65 +423,91 @@ export const TrialList: FC<{ studyDetail: StudyDetail | null }> = ({
</Menu>
</ListSubheader>
<Divider />
{trials.map((trial) => {
return (
<ListItem key={trial.trial_id} disablePadding>
<ListItemButton
onClick={(e) => {
if (e.shiftKey) {
let next: number[]
const selectedNumbers = selected.map((t) => t.number)
const alreadySelected =
selectedNumbers.findIndex((n) => n === trial.number) >=
0
if (alreadySelected) {
next = selectedNumbers.filter((n) => n !== trial.number)
} else {
next = [...selectedNumbers, trial.number]
}
navigate(
getTrialListLink(trial.study_id, excludedStates, next)
)
} else {
navigate(
getTrialListLink(trial.study_id, excludedStates, [
trial.number,
])
)
}
}}
selected={
selected.findIndex((t) => t.number === trial.number) !== -1
}
<Box
sx={{
width: "100%",
height: `${rowVirtualizer.getTotalSize()}px`,
position: "relative",
}}
>
{rowVirtualizer.getVirtualItems().map((virtualItem) => {
const trial = trials[virtualItem.index]
return (
<ListItem
key={trial.trial_id}
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
position: "absolute",
top: 0,
left: 0,
transform: `translateY(${virtualItem.start}px)`,
}}
disablePadding
>
<ListItemText primary={`Trial ${trial.number}`} />
<Box>
<Chip
color={getChipColor(trial.state)}
label={trial.state}
sx={{ margin: theme.spacing(0) }}
size="small"
variant="outlined"
/>
{isBestTrial(trial.trial_id) ? (
<ListItemButton
onClick={(e) => {
if (e.shiftKey) {
let next: number[]
const selectedNumbers = selected.map((t) => t.number)
const alreadySelected =
selectedNumbers.findIndex(
(n) => n === trial.number
) >= 0
if (alreadySelected) {
next = selectedNumbers.filter(
(n) => n !== trial.number
)
} else {
next = [...selectedNumbers, trial.number]
}
navigate(
getTrialListLink(trial.study_id, excludedStates, next)
)
} else {
navigate(
getTrialListLink(trial.study_id, excludedStates, [
trial.number,
])
)
}
}}
selected={
selected.findIndex((t) => t.number === trial.number) !==
-1
}
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
}}
>
<ListItemText primary={`Trial ${trial.number}`} />
<Box>
<Chip
label={"Best Trial"}
color="secondary"
sx={{ marginLeft: theme.spacing(1) }}
color={getChipColor(trial.state)}
label={trial.state}
sx={{ margin: theme.spacing(0) }}
size="small"
variant="outlined"
/>
) : null}
</Box>
</ListItemButton>
</ListItem>
)
})}
{isBestTrial(trial.trial_id) ? (
<Chip
label={"Best Trial"}
color="secondary"
sx={{ marginLeft: theme.spacing(1) }}
size="small"
variant="outlined"
/>
) : null}
</Box>
</ListItemButton>
</ListItem>
)
})}
</Box>
</List>
</Box>
<Divider orientation="vertical" flexItem />
+66 -90
View File
@@ -25,29 +25,37 @@ export const TrialTable: FC<{
toCellValue: (i) => trials[i].state.toString(),
},
]
const valueComparator = (
firstVal?: TrialValueNumber,
secondVal?: TrialValueNumber,
ascending: boolean = true
): number => {
if (firstVal === secondVal) {
return 0
}
if (firstVal === undefined) {
return ascending ? -1 : 1
} else if (secondVal === undefined) {
return ascending ? 1 : -1
}
if (firstVal === "-inf" || secondVal === "inf") {
return 1
} else if (secondVal === "-inf" || firstVal === "inf") {
return -1
}
return firstVal < secondVal ? 1 : -1
}
if (studyDetail === null || studyDetail.directions.length === 1) {
columns.push({
field: "values",
label: "Value",
sortable: true,
less: (firstEl, secondEl, ascending): number => {
const firstVal = firstEl.values?.[0]
const secondVal = secondEl.values?.[0]
if (firstVal === secondVal) {
return 0
}
if (firstVal === undefined) {
return ascending ? -1 : 1
} else if (secondVal === undefined) {
return ascending ? 1 : -1
}
if (firstVal === "-inf" || secondVal === "inf") {
return 1
} else if (secondVal === "-inf" || firstVal === "inf") {
return -1
}
return firstVal < secondVal ? 1 : -1
return valueComparator(
firstEl.values?.[0],
secondEl.values?.[0],
ascending
)
},
toCellValue: (i) => {
if (trials[i].values === undefined) {
@@ -66,23 +74,11 @@ export const TrialTable: FC<{
: `Objective ${objectiveId}`,
sortable: true,
less: (firstEl, secondEl, ascending): number => {
const firstVal = firstEl.values?.[objectiveId]
const secondVal = secondEl.values?.[objectiveId]
if (firstVal === secondVal) {
return 0
}
if (firstVal === undefined) {
return ascending ? -1 : 1
} else if (secondVal === undefined) {
return ascending ? 1 : -1
}
if (firstVal === "-inf" || secondVal === "inf") {
return 1
} else if (secondVal === "-inf" || firstVal === "inf") {
return -1
}
return firstVal < secondVal ? 1 : -1
return valueComparator(
firstEl.values?.[objectiveId],
secondEl.values?.[objectiveId],
ascending
)
},
toCellValue: (i) => {
if (trials[i].values === undefined) {
@@ -93,55 +89,41 @@ export const TrialTable: FC<{
}))
columns.push(...objectiveColumns)
}
if (
studyDetail?.union_search_space.length ===
const isDynamicSpace =
studyDetail?.union_search_space.length !==
studyDetail?.intersection_search_space.length
) {
studyDetail?.intersection_search_space.forEach((s) => {
const sortable = s.distribution.type !== "CategoricalDistribution"
const filterChoices =
s.distribution.type === "CategoricalDistribution"
? s.distribution.choices.map((c) => c.value)
: undefined
columns.push({
field: "params",
label: `Param ${s.name}`,
toCellValue: (i) =>
trials[i].params.find((p) => p.name === s.name)
?.param_external_value || null,
sortable: sortable,
filterChoices: filterChoices,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
less: (firstEl, secondEl, _): number => {
const firstVal = firstEl.params.find(
(p) => p.name === s.name
)?.param_internal_value
const secondVal = secondEl.params.find(
(p) => p.name === s.name
)?.param_internal_value
if (firstVal === secondVal) {
return 0
} else if (firstVal && secondVal) {
return firstVal < secondVal ? 1 : -1
} else if (firstVal) {
return -1
} else {
return 1
}
},
})
})
} else {
studyDetail?.union_search_space.forEach((s) => {
const sortable = s.distribution.type !== "CategoricalDistribution"
const filterChoices: (string | null)[] | undefined =
s.distribution.type === "CategoricalDistribution"
? s.distribution.choices.map((c) => c.value)
: undefined
const hasMissingValue = trials.some(
(t) => !t.params.some((p) => p.name === s.name)
)
if (filterChoices !== undefined && isDynamicSpace && hasMissingValue) {
filterChoices.push(null)
}
columns.push({
field: "params",
label: "Params",
label: `Param ${s.name}`,
toCellValue: (i) =>
trials[i].params
.map((p) => p.name + ": " + p.param_external_value)
.join(", "),
trials[i].params.find((p) => p.name === s.name)?.param_external_value ||
null,
sortable: sortable,
filterChoices: filterChoices,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
less: (firstEl, secondEl, _): number => {
const firstVal = firstEl.params.find(
(p) => p.name === s.name
)?.param_internal_value
const secondVal = secondEl.params.find(
(p) => p.name === s.name
)?.param_internal_value
return valueComparator(firstVal, secondVal)
},
})
}
})
studyDetail?.union_user_attrs.forEach((attr_spec) => {
columns.push({
@@ -153,22 +135,16 @@ export const TrialTable: FC<{
sortable: attr_spec.sortable,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
less: (firstEl, secondEl, _): number => {
const firstVal = firstEl.user_attrs.find(
const firstValString = firstEl.user_attrs.find(
(attr) => attr.key === attr_spec.key
)?.value
const secondVal = secondEl.user_attrs.find(
const secondValString = secondEl.user_attrs.find(
(attr) => attr.key === attr_spec.key
)?.value
if (firstVal === secondVal) {
return 0
} else if (firstVal && secondVal) {
return Number(firstVal) < Number(secondVal) ? 1 : -1
} else if (firstVal) {
return -1
} else {
return 1
}
return valueComparator(
Number(firstValString) ?? firstValString,
Number(secondValString) ?? secondValString
)
},
})
})
@@ -0,0 +1,40 @@
import { useEffect } from "react"
import { useSnackbar } from "notistack"
import { getParamImportances } from "../apiClient"
import { useQuery } from "@tanstack/react-query"
import { AxiosError } from "axios"
export const useParamImportance = ({
numCompletedTrials,
studyId,
}: { numCompletedTrials: number; studyId: number }) => {
const { enqueueSnackbar } = useSnackbar()
const { data, isLoading, error } = useQuery<
ParamImportance[][],
AxiosError<{ reason: string }>
>({
queryKey: ["paramImportance", studyId, numCompletedTrials],
queryFn: () => getParamImportances(studyId),
staleTime: Infinity,
gcTime: 30 * 60 * 1000, // 30 minutes
})
useEffect(() => {
if (error) {
const reason = error.response?.data.reason
enqueueSnackbar(
`Failed to load hyperparameter importance (reason=${reason})`,
{
variant: "error",
}
)
}
}, [error])
return {
importances: data,
isLoading,
error,
}
}
+37
View File
@@ -0,0 +1,37 @@
import * as plotly from "plotly.js-dist-min"
import { useQuery } from "@tanstack/react-query"
import { AxiosError } from "axios"
import { PlotType, getPlotAPI } from "../apiClient"
export const usePlot = ({
numCompletedTrials,
studyId,
plotType,
}: {
numCompletedTrials: number
studyId: number | undefined
plotType: PlotType
}) => {
const { data, isLoading, error } = useQuery<
{ data: plotly.Data[]; layout: plotly.Layout },
AxiosError
>({
enabled: studyId !== undefined,
queryKey: ["plot", studyId, numCompletedTrials, plotType],
queryFn: () => {
if (studyId === undefined) {
return Promise.reject(new Error("Invalid studyId"))
}
return getPlotAPI(studyId, plotType)
},
staleTime: Infinity,
gcTime: 30 * 60 * 1000, // 30 minutes
})
return {
data: data?.data,
layout: data?.layout,
isLoading,
error,
}
}
+16 -20
View File
@@ -3,7 +3,6 @@ import {
LightColorTemplates,
DarkColorTemplates,
} from "./components/PlotlyColorTemplates"
import { useQuery } from "./urlQuery"
export const studySummariesState = atom<StudySummary[]>({
key: "studySummaries",
@@ -22,11 +21,6 @@ export const trialsUpdatingState = atom<{
default: {},
})
export const paramImportanceState = atom<StudyParamImportance>({
key: "paramImportance",
default: {},
})
// TODO(c-bata): Consider representing the state as boolean.
export const reloadIntervalState = atom<number>({
key: "reloadInterval",
@@ -48,14 +42,19 @@ export const artifactIsAvailable = atom<boolean>({
default: false,
})
export const plotlyColorTheme = atom<PlotlyColorTheme>({
key: "plotlyDarkColorScale",
export const plotlyColorThemeState = atom<PlotlyColorTheme>({
key: "plotlyColorThemeState",
default: {
dark: "default",
light: "default",
},
})
export const plotBackendRenderingState = atom<boolean>({
key: "plotBackendRendering",
default: false,
})
export const plotlypyIsAvailableState = atom<boolean>({
key: "plotlypyIsAvailable",
default: true,
@@ -66,6 +65,11 @@ export const studySummariesLoadingState = atom<boolean>({
default: false,
})
export const studyDetailLoadingState = atom<Record<number, boolean>>({
key: "studyDetailLoading",
default: {},
})
export const useStudyDetailValue = (studyId: number): StudyDetail | null => {
const studyDetails = useRecoilValue<StudyDetails>(studyDetailsState)
return studyDetails[studyId] || null
@@ -81,14 +85,6 @@ export const useTrialUpdatingValue = (trialId: number): boolean => {
return updating[trialId] || false
}
export const useParamImportanceValue = (
studyId: number
): ParamImportance[][] | null => {
const studyParamImportance =
useRecoilValue<StudyParamImportance>(paramImportanceState)
return studyParamImportance[studyId] || null
}
export const useStudyDirections = (
studyId: number
): StudyDirection[] | null => {
@@ -119,7 +115,7 @@ export const useArtifacts = (studyId: number, trialId: number): Artifact[] => {
}
export const usePlotlyColorTheme = (mode: string): Partial<Plotly.Template> => {
const theme = useRecoilValue(plotlyColorTheme)
const theme = useRecoilValue(plotlyColorThemeState)
if (mode === "dark") {
return DarkColorTemplates[theme.dark]
} else {
@@ -128,10 +124,10 @@ export const usePlotlyColorTheme = (mode: string): Partial<Plotly.Template> => {
}
export const useBackendRender = (): boolean => {
const query = useQuery()
const plotlypyIsAvailable = useRecoilValue<boolean>(plotlypyIsAvailableState)
const plotBackendRendering = useRecoilValue(plotBackendRenderingState)
const plotlypyIsAvailable = useRecoilValue(plotlypyIsAvailableState)
if (query.get("plotlypy_rendering") === "true") {
if (plotBackendRendering) {
if (plotlypyIsAvailable) {
return true
}
@@ -2,10 +2,7 @@ import React from "react"
global.URL.createObjectURL = jest.fn()
import { cleanup, render } from "@testing-library/react"
import {
DataGrid,
DataGridColumn,
} from "../optuna_dashboard/ts/components/DataGrid"
import { DataGrid, DataGridColumn } from "../components/DataGrid"
afterEach(cleanup)
@@ -1,4 +1,4 @@
import { mergeUnionSearchSpace } from "../optuna_dashboard/ts/searchSpace"
import { mergeUnionSearchSpace } from "../searchSpace"
global.URL.createObjectURL = jest.fn()
+5 -6
View File
@@ -226,10 +226,6 @@ type StudyDetails = {
[study_id: string]: StudyDetail
}
type StudyParamImportance = {
[study_id: string]: ParamImportance[][]
}
type PreferenceHistory = {
id: string
candidates: number[]
@@ -240,7 +236,10 @@ type PreferenceHistory = {
is_removed: boolean
}
type PlotlyColorThemeDark = "default"
type PlotlyColorThemeLight = "default" | "seaborn" | "presentation" | "ggplot2"
type PlotlyColorTheme = {
dark: string
light: string
dark: PlotlyColorThemeDark
light: PlotlyColorThemeLight
}
@@ -6,7 +6,7 @@
"noUnusedLocals": true,
"noImplicitThis": true,
"alwaysStrict": true,
"outDir": "./optuna_dashboard/public/",
"outDir": "./public/",
"paths": {
"plotly.js-dist-min": ["node_modules/@types/plotly.js"]
},
@@ -21,11 +21,11 @@
"strict": true
},
"files": [
"./optuna_dashboard/ts/index.tsx"
"./ts/index.tsx"
],
"include": [
"./optuna_dashboard/ts/types/**/*",
"./typescript_tests/**/*"
"./ts/types/**/*",
"./ts/tests/**/*"
],
"types": ["node"]
}
@@ -28,9 +28,9 @@ const typeScriptLoader =
var config = {
mode,
entry: [__dirname + "/optuna_dashboard/ts/index.tsx"],
entry: [__dirname + "/ts/index.tsx"],
output: {
path: __dirname + "/optuna_dashboard/public/",
path: __dirname + "/public/",
filename: "bundle.js",
publicPath: "/public/",
},
+298 -25309
View File
File diff suppressed because it is too large Load Diff
+4 -63
View File
@@ -1,73 +1,14 @@
{
"name": "optuna-dashboard",
"private": true,
"version": "0.0.1",
"description": "Dashboard for Optuna",
"main": "index.js",
"scripts": {
"fmt": "biome format --write .",
"lint": "npm run lint:eslint && npm run lint:fmt",
"fmt": "biome format --write . && biome check standalone_app vscode tslib --apply",
"lint": "npm run lint:eslint && npm run lint:biome",
"lint:eslint": "eslint . --ext .ts,.tsx --max-warnings 0",
"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",
"build:prd": "NODE_ENV=production webpack",
"test": "jest typescript_tests"
},
"author": "Masashi Shibata",
"license": "MIT",
"dependencies": {
"@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.28.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.21.3",
"react-syntax-highlighter": "^15.5.0",
"reactflow": "^11.10.3",
"recoil": "^0.7.7",
"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"
"lint:biome": "biome format . && biome ci standalone_app vscode tslib"
},
"devDependencies": {
"@babel/core": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"@biomejs/biome": "1.5.3",
"@testing-library/react": "^14.1.2",
"@types/jest": "^29.5.11",
"@types/plotly.js": "^2.12.32",
"@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"
"eslint": "^8.56.0"
}
}
+1
View File
@@ -75,6 +75,7 @@ package-data = { "optuna_dashboard" = ["public/*", "img/*", "index.html"] }
[tool.setuptools.packages.find]
include = ["optuna_dashboard*"]
exclude = ["optuna_dashboard.node_modules*", "optuna_dashboard.ts*"]
[project.urls]
"Homepage" = "https://github.com/optuna/optuna-dashboard"
+18 -24
View File
@@ -5,9 +5,9 @@ import sys
import numpy as np
import optuna
from optuna_dashboard._serializer import serialize_attrs
from optuna_dashboard._serializer import serialize_frozen_study
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._storage import get_studies
from optuna_dashboard.preferential import create_study
from packaging import version
import pytest
@@ -60,26 +60,20 @@ def test_serialize_numpy_floating() -> None:
def test_get_study_detail_is_preferential() -> None:
storage = optuna.storages.InMemoryStorage()
study = create_study(n_generate=4, storage=storage)
study_summaries = get_study_summaries(storage)
assert len(study_summaries) == 1
studies = get_studies(storage)
assert len(studies) == 1
study_summary = study_summaries[0]
study_detail = serialize_study_detail(
study_summary, [], study.trials, [], [], [], False, {}, []
)
study_detail = serialize_study_detail(studies[0], [], study.trials, [], [], [], False, {}, [])
assert study_detail["is_preferential"]
def test_get_study_detail_is_not_preferential() -> None:
storage = optuna.storages.InMemoryStorage()
study = optuna.create_study(storage=storage)
study_summaries = get_study_summaries(storage)
assert len(study_summaries) == 1
studies = get_studies(storage)
assert len(studies) == 1
study_summary = study_summaries[0]
study_detail = serialize_study_detail(
study_summary, [], study.trials, [], [], [], False, {}, []
)
study_detail = serialize_study_detail(studies[0], [], study.trials, [], [], [], False, {}, [])
assert not study_detail["is_preferential"]
@@ -87,20 +81,20 @@ def test_get_study_detail_is_not_preferential() -> None:
@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:
def test_get_study_is_preferential() -> None:
storage = optuna.storages.InMemoryStorage()
create_study(n_generate=4, storage=storage)
study_summaries = get_study_summaries(storage)
assert len(study_summaries) == 1
studies = get_studies(storage)
assert len(studies) == 1
study_summary = serialize_study_summary(study_summaries[0])
assert study_summary["is_preferential"]
serialized = serialize_frozen_study(studies[0])
assert serialized["is_preferential"]
def test_get_study_summary_is_not_preferential() -> None:
def test_get_study_is_not_preferential() -> None:
storage = optuna.storages.InMemoryStorage()
optuna.create_study(storage=storage)
study_summaries = get_study_summaries(storage)
assert len(study_summaries) == 1
study_summary = serialize_study_summary(study_summaries[0])
assert not study_summary["is_preferential"]
studies = get_studies(storage)
assert len(studies) == 1
serialized = serialize_frozen_study(studies[0])
assert not serialized["is_preferential"]
+1 -1
View File
@@ -4,4 +4,4 @@ ignore =
W503
max-line-length = 99
statistics = True
exclude = venv,build
exclude = venv,build,node_modules
-5
View File
@@ -1,5 +0,0 @@
from setuptools import setup
if __name__ == "__main__":
setup()
-19
View File
@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Optuna Dashboard (Wasm ver.)</title>
<script defer type="module" src="/optuna-dashboard/public/bundle.js"></script>
<link rel="icon" href="/optuna-dashboard/favicon.ico" />
<!-- ogp -->
<meta property="og:title" content="Optuna Dashboard (Wasm ver.)" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://optuna.github.io/optuna-dashboard/" />
<meta property="og:image" content="/img/ogp_image.png" />
<meta property="og:description" content="Web Dashboard for Optuna, Python library for hyperparameter optimization" />
</head>
<body>
<div id="root"></div>
</body>
</html>
+7 -2
View File
@@ -3,8 +3,13 @@
<head>
<meta charset="utf-8" />
<title>Optuna Dashboard (Wasm ver.)</title>
<script defer type="module" src="/public/bundle.js"></script>
<link rel="icon" href="/favicon.ico" />
<script defer type="module" src="/src/browser_app_entry.tsx"></script>
<link rel="icon" href="/public/favicon.ico" />
<meta property="og:title" content="Optuna Dashboard (Wasm ver.)" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://optuna.github.io/optuna-dashboard/" />
<meta property="og:description" content="Web Dashboard for Optuna, Python library for hyperparameter optimization" />
</head>
<body>
<div id="root"></div>
+2304 -1329
View File
File diff suppressed because it is too large Load Diff
+19 -23
View File
@@ -4,38 +4,34 @@
"version": "0.0.0",
"description": "",
"scripts": {
"watch": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack --watch",
"serve": "python3 -m http.server 9000 --directory .",
"build:dev": "NODE_ENV=development TYPESCRIPT_LOADER=esbuild-loader webpack",
"build:prd": "NODE_ENV=production webpack"
"watch": "vite",
"build:vscode": "webpack"
},
"devDependencies": {
"@types/plotly.js": "^2.12.18",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@types/plotly.js": "^2.29.2",
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21",
"@types/react-router-dom": "^5.3.3",
"compression-webpack-plugin": "^10.0.0",
"esbuild-loader": "^3.0.1",
"prettier": "^2.8.8",
"ts-loader": "^9.4.2",
"typescript": "^5.0.4",
"webpack": "^5.82.1",
"webpack-cli": "^5.1.1"
"@vitejs/plugin-react": "^4.2.1",
"compression-webpack-plugin": "^11.1.0",
"esbuild-loader": "^4.0.3",
"vite": "^5.1.5",
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@emotion/react": "^11.10.8",
"@emotion/styled": "^11.10.8",
"@mui/icons-material": "^5.11.16",
"@mui/lab": "^5.0.0-alpha.128",
"@mui/material": "^5.12.2",
"@sqlite.org/sqlite-wasm": "^3.41.2-build11",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.12",
"@mui/lab": "^5.0.0-alpha.167",
"@mui/material": "^5.15.12",
"@optuna/storage": "../tslib/storage",
"module-workers-polyfill": "^0.3.2",
"notistack": "^3.0.1",
"optuna": "../rustlib/pkg",
"plotly.js-dist-min": "^2.22.0",
"plotly.js-dist-min": "^2.30.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.11.0",
"recoil": "^0.7.7"
"react-router-dom": "^6.22.3"
}
}

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+4 -4
View File
@@ -1,13 +1,13 @@
import React from "react"
import ReactDOM from "react-dom/client"
import "./index.css"
import { App } from "./components/App"
import { RecoilRoot } from "recoil"
import { StorageProvider } from "./components/StorageProvider"
import "./index.css"
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RecoilRoot>
<StorageProvider>
<App />
</RecoilRoot>
</StorageProvider>
</React.StrictMode>
)
+8 -8
View File
@@ -1,15 +1,15 @@
import React, { FC, useMemo, useState, useEffect } from "react"
import { HashRouter as Router, Routes, Route } from "react-router-dom"
import { SnackbarProvider } from "notistack"
import blue from "@mui/material/colors/blue"
import pink from "@mui/material/colors/pink"
import {
createTheme,
useMediaQuery,
ThemeProvider,
Box,
CssBaseline,
ThemeProvider,
createTheme,
useMediaQuery,
} from "@mui/material"
import blue from "@mui/material/colors/blue"
import pink from "@mui/material/colors/pink"
import { SnackbarProvider } from "notistack"
import React, { FC, useMemo, useState, useEffect } from "react"
import { HashRouter as Router, Route, Routes } from "react-router-dom"
import { StudyDetail } from "./StudyDetail"
import { StudyList } from "./StudyList"
+11 -9
View File
@@ -1,5 +1,9 @@
import React from "react"
import { Clear } from "@mui/icons-material"
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"
import {
Collapse,
IconButton,
Table,
TableBody,
TableCell,
@@ -8,18 +12,14 @@ import {
TablePagination,
TableRow,
TableSortLabel,
Collapse,
IconButton,
useTheme,
} from "@mui/material"
import { styled } from "@mui/system"
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"
import { Clear } from "@mui/icons-material"
import React from "react"
type Order = "asc" | "desc"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
type Value = any
const defaultRowsPerPageOption = [10, 50, 100, { label: "All", value: -1 }]
@@ -97,7 +97,7 @@ function DataGrid<T>(props: {
}
const filteredRows = rows.filter((row, rowIdx) => {
if (defaultFilter !== undefined && defaultFilter(row)) {
if (defaultFilter?.(row)) {
return false
}
return filters.length === 0
@@ -162,6 +162,7 @@ function DataGrid<T>(props: {
{collapseBody ? <TableCell /> : null}
{columns.map((column, columnIdx) => (
<TableCell
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
key={columnIdx}
padding={column.padding || "normal"}
sortDirection={orderBy === column.field ? order : false}
@@ -378,4 +379,5 @@ const isNumber = (
return typeof rowsPerPage === "number"
}
export { DataGrid, DataGridColumn }
export { DataGrid }
export type { DataGridColumn }
@@ -1,28 +0,0 @@
import React, { FC, useEffect } from "react"
import { TextField, TextFieldProps } from "@mui/material"
export const DebouncedInputTextField: FC<{
onChange: (s: string, valid: boolean) => void
delay: number
textFieldProps: TextFieldProps
}> = ({ onChange, delay, textFieldProps }) => {
const [text, setText] = React.useState<string>("")
const [valid, setValidity] = React.useState<boolean>(true)
useEffect(() => {
const timer = setTimeout(() => {
onChange(text, valid)
}, delay)
return () => {
clearTimeout(timer)
}
}, [text, delay])
return (
<TextField
onChange={(e) => {
setText(e.target.value)
setValidity(e.target.validity.valid)
}}
{...textFieldProps}
/>
)
}
+13 -12
View File
@@ -1,20 +1,20 @@
import * as plotly from "plotly.js-dist-min"
import React, { ChangeEvent, FC, useEffect, useState } from "react"
import {
Grid,
FormControl,
FormLabel,
FormControlLabel,
Checkbox,
FormControl,
FormControlLabel,
FormLabel,
Grid,
MenuItem,
Switch,
Select,
Radio,
RadioGroup,
Typography,
Select,
SelectChangeEvent,
Switch,
Typography,
useTheme,
} from "@mui/material"
import * as plotly from "plotly.js-dist-min"
import React, { ChangeEvent, FC, useEffect, useState } from "react"
import { plotlyDarkTemplate } from "../PlotlyDarkMode"
const plotDomId = "plot-history"
@@ -91,6 +91,7 @@ export const PlotHistory: FC<{
<FormLabel component="legend">Objective ID:</FormLabel>
<Select value={objectiveId} onChange={handleObjectiveChange}>
{study.directions.map((d, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<MenuItem value={i} key={i}>
{i}
</MenuItem>
@@ -178,8 +179,8 @@ const filterFunc = (trial: Trial, objectiveId: number): boolean => {
}
return (
trial.values.length > objectiveId &&
trial.values[objectiveId] !== "inf" &&
trial.values[objectiveId] !== "-inf"
trial.values[objectiveId] !== Infinity &&
trial.values[objectiveId] !== -Infinity
)
}
@@ -244,7 +245,7 @@ const plotHistory = (
return null
}
const value = trial.values[objectiveId]
if (value === "inf" || value === "-inf") {
if (value === Infinity || value === -Infinity) {
return null
}
return value
@@ -1,14 +1,13 @@
import { Box, Typography, useTheme } from "@mui/material"
import init, { wasm_fanova_calculate } from "optuna"
import * as plotly from "plotly.js-dist-min"
import React, { FC, useEffect, useState } from "react"
import { Typography, useTheme, Box } from "@mui/material"
import init, { wasm_fanova_calculate } from "optuna"
import { plotlyDarkTemplate } from "../PlotlyDarkMode"
const plotDomId = "graph-hyperparameter-importances"
export const PlotImportance: FC<{ study: Study }> = ({ study }) => {
const theme = useTheme()
const nObjectives = study.directions.length
const objectiveNames: string[] = study.directions.map(
(d, i) => `Objective ${i}`
)
@@ -50,13 +49,13 @@ export const PlotImportance: FC<{ study: Study }> = ({ study }) => {
}
run_wasm()
}, [])
}, [study])
useEffect(() => {
if (importance.length > 0) {
plotParamImportancesBeta(importance, objectiveNames, theme.palette.mode)
}
}, [nObjectives, importance, theme.palette.mode])
}, [objectiveNames, importance, theme.palette.mode])
return (
<>
@@ -80,8 +79,8 @@ const filterFunc = (trial: Trial, objectiveId: number): boolean => {
}
return (
trial.values.length > objectiveId &&
trial.values[objectiveId] !== "inf" &&
trial.values[objectiveId] !== "-inf"
trial.values[objectiveId] !== Infinity &&
trial.values[objectiveId] !== -Infinity
)
}
@@ -1,6 +1,6 @@
import { Box, Typography, useTheme } from "@mui/material"
import * as plotly from "plotly.js-dist-min"
import React, { FC, useEffect } from "react"
import { Box, Typography, useTheme } from "@mui/material"
import { plotlyDarkTemplate } from "../PlotlyDarkMode"
const plotDomId = "graph-intermediate-values"
@@ -20,7 +20,7 @@ export const PlotIntermediateValues: FC<{
!includePruned,
logScale
)
}, [trials, theme.palette.mode, false, includePruned, logScale])
}, [trials, theme.palette.mode, includePruned, logScale])
return (
<>
@@ -80,7 +80,10 @@ const plotIntermediateValue = (
)
const plotData: Partial<plotly.PlotData>[] = filteredTrials.map((trial) => {
const values = trial.intermediate_values.filter(
(iv) => iv.value !== "inf" && iv.value !== "-inf" && iv.value !== "nan"
(iv) =>
iv.value !== Infinity &&
iv.value !== -Infinity &&
!Number.isNaN(iv.value)
)
return {
x: values.map((iv) => iv.step),
+14 -21
View File
@@ -1,15 +1,4 @@
import React, {
ChangeEvent,
DragEventHandler,
FC,
MouseEventHandler,
useRef,
useState,
} from "react"
import { loadSQLite3Storage } from "../sqlite3"
import { loadJournalStorage } from "../journalStorage"
import { useSetRecoilState } from "recoil"
import { studiesState } from "../state"
import UploadFileIcon from "@mui/icons-material/UploadFile"
import {
Card,
CardActionArea,
@@ -17,13 +6,22 @@ import {
Typography,
useTheme,
} from "@mui/material"
import UploadFileIcon from "@mui/icons-material/UploadFile"
import React, {
ChangeEvent,
DragEventHandler,
FC,
MouseEventHandler,
useRef,
useState,
useContext,
} from "react"
import { StorageContext, getStorage } from "./StorageProvider"
export const StorageLoader: FC = () => {
const theme = useTheme()
const [dragOver, setDragOver] = useState<boolean>(false)
const { setStorage } = useContext(StorageContext)
const setStudies = useSetRecoilState<Study[]>(studiesState)
const inputRef = useRef<HTMLInputElement>(null)
const loadStorageFromFile = (file: File): void => {
@@ -31,13 +29,8 @@ export const StorageLoader: FC = () => {
r.addEventListener("load", () => {
const arrayBuffer = r.result as ArrayBuffer | null
if (arrayBuffer !== null) {
const header = new Uint8Array(arrayBuffer, 0, 16)
const headerString = new TextDecoder().decode(header)
if (headerString === "SQLite format 3\u0000") {
loadSQLite3Storage(arrayBuffer, setStudies)
} else {
loadJournalStorage(arrayBuffer, setStudies)
}
const s = getStorage(arrayBuffer)
setStorage(s)
}
})
r.readAsArrayBuffer(file)
@@ -0,0 +1,31 @@
import { JournalFileStorage } from "@optuna/storage"
import { SQLite3Storage } from "@optuna/storage"
import React, { FC, createContext, useState } from "react"
export const StorageContext = createContext<{
storage: OptunaStorage | null
setStorage: (storage: OptunaStorage) => void
}>({
storage: null,
setStorage: () => {},
})
export const getStorage = (arrayBuffer: ArrayBuffer): OptunaStorage => {
const header = new Uint8Array(arrayBuffer, 0, 16)
const headerString = new TextDecoder().decode(header)
if (headerString === "SQLite format 3\u0000") {
return new SQLite3Storage(arrayBuffer)
}
return new JournalFileStorage(arrayBuffer)
}
export const StorageProvider: FC<{
children: React.ReactNode
}> = ({ children }) => {
const [storage, setStorage] = useState<OptunaStorage | null>(null)
return (
<StorageContext.Provider value={{ storage, setStorage }}>
{children}
</StorageContext.Provider>
)
}
+33 -27
View File
@@ -1,31 +1,25 @@
import React, { FC } from "react"
import { Link, useParams } from "react-router-dom"
import {
AppBar,
Typography,
Container,
Toolbar,
Box,
IconButton,
useTheme,
Card,
CardContent,
} from "@mui/material"
import Grid2 from "@mui/material/Unstable_Grid2"
import { Home } from "@mui/icons-material"
import Brightness4Icon from "@mui/icons-material/Brightness4"
import Brightness7Icon from "@mui/icons-material/Brightness7"
import { useRecoilValue } from "recoil"
import { studiesState } from "../state"
import { TrialTable } from "./TrialTable"
import {
AppBar,
Box,
Card,
CardContent,
Container,
IconButton,
Toolbar,
Typography,
useTheme,
} from "@mui/material"
import Grid2 from "@mui/material/Unstable_Grid2"
import React, { FC, useContext, useState, useEffect } from "react"
import { Link, useParams } from "react-router-dom"
import { PlotHistory } from "./PlotHistory"
import { PlotImportance } from "./PlotImportance"
import { PlotIntermediateValues } from "./PlotIntermediateValues"
const useStudyValue = (idx: number): Study | null => {
const studies = useRecoilValue<Study[]>(studiesState)
return studies[idx] || null
}
import { StorageContext } from "./StorageProvider"
import { TrialTable } from "./TrialTable"
export const StudyDetail: FC<{
toggleColorMode: () => void
@@ -33,14 +27,26 @@ export const StudyDetail: FC<{
const theme = useTheme()
const { idx } = useParams<{ idx: string }>()
const idxNumber = parseInt(idx || "", 10)
const study = useStudyValue(idxNumber)
const { storage } = useContext(StorageContext)
const [study, setStudy] = useState<Study | null>(null)
useEffect(() => {
const fetchStudy = async () => {
if (storage === null) {
return
}
const study = await storage.getStudy(idxNumber)
setStudy(study)
}
fetchStudy()
}, [storage, idxNumber])
return (
<div>
<>
<AppBar position="static">
<Container
sx={{
["@media (min-width: 1280px)"]: {
"@media (min-width: 1280px)": {
maxWidth: "100%",
},
}}
@@ -80,7 +86,7 @@ export const StudyDetail: FC<{
</AppBar>
<Container
sx={{
["@media (min-width: 1280px)"]: {
"@media (min-width: 1280px)": {
maxWidth: "100%",
},
}}
@@ -133,6 +139,6 @@ export const StudyDetail: FC<{
</Card>
</>
</Container>
</div>
</>
)
}
+79 -61
View File
@@ -1,52 +1,72 @@
import React, { FC, useState } from "react"
import {
AppBar,
Typography,
Container,
Toolbar,
Box,
IconButton,
MenuItem,
useTheme,
Card,
CardContent,
CardActionArea,
TextField,
InputAdornment,
SvgIcon,
} from "@mui/material"
import { styled } from "@mui/system"
import SortIcon from "@mui/icons-material/Sort"
import { Search } from "@mui/icons-material"
import Brightness4Icon from "@mui/icons-material/Brightness4"
import Brightness7Icon from "@mui/icons-material/Brightness7"
import { useRecoilValue } from "recoil"
import { studiesState } from "../state"
import SortIcon from "@mui/icons-material/Sort"
import {
AppBar,
Box,
Card,
CardActionArea,
CardContent,
Container,
IconButton,
InputAdornment,
MenuItem,
SvgIcon,
TextField,
Toolbar,
Typography,
useTheme,
} from "@mui/material"
import { styled } from "@mui/system"
import React, {
FC,
useEffect,
useContext,
useState,
useMemo,
useDeferredValue,
} from "react"
import { Link } from "react-router-dom"
import { DebouncedInputTextField } from "./Debounce"
import { Search } from "@mui/icons-material"
import { StorageLoader } from "./StorageLoader"
import { StorageContext } from "./StorageProvider"
export const StudyList: FC<{
toggleColorMode: () => void
}> = ({ toggleColorMode }) => {
const theme = useTheme()
const studies = useRecoilValue<Study[]>(studiesState)
const { storage } = useContext(StorageContext)
const [studies, setStudies] = useState<StudySummary[]>([])
const [studyFilterText, setStudyFilterText] = useState<string>("")
const [_studyFilterText, setStudyFilterText] = useState<string>("")
const [sortBy, setSortBy] = useState<"id-asc" | "id-desc">("id-asc")
const studyFilter = (row: Study): boolean => {
const keywords = studyFilterText.split(" ")
return !keywords.every((k) => {
if (k === "") {
return true
const studyFilterText = useDeferredValue(_studyFilterText)
useEffect(() => {
const fetchStudies = async () => {
if (storage === null) {
return
}
return row.study_name.indexOf(k) >= 0
})
}
let filteredStudies: Study[] = studies.filter((s) => !studyFilter(s))
if (sortBy === "id-desc") {
filteredStudies = filteredStudies.reverse()
}
const studies = await storage.getStudies()
setStudies(studies)
}
fetchStudies()
}, [storage])
const filteredStudies = useMemo(() => {
const studyFilter = (row: StudySummary): boolean => {
const keywords = studyFilterText.split(" ")
return !keywords.every((k) => {
if (k === "") {
return true
}
return row.study_name.indexOf(k) >= 0
})
}
let filteredStudies: StudySummary[] = studies.filter((s) => !studyFilter(s))
if (sortBy === "id-desc") {
filteredStudies = filteredStudies.reverse()
}
return filteredStudies
}, [studyFilterText, studies, sortBy])
const Select = styled(TextField)(({ theme }) => ({
"& .MuiInputBase-input": {
@@ -93,7 +113,7 @@ export const StudyList: FC<{
<AppBar position="static">
<Container
sx={{
["@media (min-width: 1280px)"]: {
"@media (min-width: 1280px)": {
maxWidth: "100%",
},
}}
@@ -123,7 +143,7 @@ export const StudyList: FC<{
</AppBar>
<Container
sx={{
["@media (min-width: 1280px)"]: {
"@media (min-width: 1280px)": {
maxWidth: "100%",
},
}}
@@ -131,26 +151,23 @@ export const StudyList: FC<{
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<Box sx={{ display: "flex" }}>
<DebouncedInputTextField
onChange={(s) => {
setStudyFilterText(s)
<TextField
onChange={(e) => {
setStudyFilterText(e.target.value)
}}
delay={500}
textFieldProps={{
fullWidth: true,
id: "search-study",
variant: "outlined",
placeholder: "Search study",
sx: { maxWidth: 500 },
InputProps: {
startAdornment: (
<InputAdornment position="start">
<SvgIcon fontSize="small" color="action">
<Search />
</SvgIcon>
</InputAdornment>
),
},
id="search-study"
variant="outlined"
placeholder="Search study"
fullWidth
sx={{ maxWidth: 500 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SvgIcon fontSize="small" color="action">
<Search />
</SvgIcon>
</InputAdornment>
),
}}
/>
{sortBySelect}
@@ -166,7 +183,7 @@ export const StudyList: FC<{
>
<CardActionArea component={Link} to={`/${idx}`}>
<CardContent>
<Typography variant="h5">
<Typography variant="h5" sx={{ wordBreak: "break-all" }}>
{study.study_id}. {study.study_name}
</Typography>
<Typography
@@ -174,8 +191,9 @@ export const StudyList: FC<{
color="text.secondary"
component="div"
>
{"Direction: " +
study.directions.map((d) => d.toUpperCase()).join(", ")}
{`Direction: ${study.directions
.map((d) => d.toUpperCase())
.join(", ")}`}
</Typography>
</CardContent>
</CardActionArea>
+23 -29
View File
@@ -1,6 +1,6 @@
import React, { FC } from "react"
import { DataGridColumn, DataGrid } from "./DataGrid"
import { DataGrid, DataGridColumn } from "./DataGrid"
export const TrialTable: FC<{
study: Study
@@ -34,13 +34,9 @@ export const TrialTable: FC<{
}
if (firstVal === undefined) {
return ascending ? -1 : 1
} else if (secondVal === undefined) {
return ascending ? 1 : -1
}
if (firstVal === "-inf" || secondVal === "inf") {
return 1
} else if (secondVal === "-inf" || firstVal === "inf") {
return -1
if (secondVal === undefined) {
return ascending ? 1 : -1
}
return firstVal < secondVal ? 1 : -1
},
@@ -66,13 +62,9 @@ export const TrialTable: FC<{
}
if (firstVal === undefined) {
return ascending ? -1 : 1
} else if (secondVal === undefined) {
return ascending ? 1 : -1
}
if (firstVal === "-inf" || secondVal === "inf") {
return 1
} else if (secondVal === "-inf" || firstVal === "inf") {
return -1
if (secondVal === undefined) {
return ascending ? 1 : -1
}
return firstVal < secondVal ? 1 : -1
},
@@ -87,6 +79,7 @@ export const TrialTable: FC<{
columns.push(...objectiveColumns)
}
// biome-ignore lint/complexity/noForEach: <explanation>
study.union_search_space.forEach((s) => {
columns.push({
field: "params",
@@ -96,8 +89,7 @@ export const TrialTable: FC<{
null,
sortable: true,
filterable: false,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
less: (firstEl, secondEl, _): number => {
less: (firstEl, secondEl): number => {
const firstVal = firstEl.params.find(
(p) => p.name === s.name
)?.param_internal_value
@@ -107,17 +99,19 @@ export const TrialTable: FC<{
if (firstVal === secondVal) {
return 0
} else if (firstVal && secondVal) {
return firstVal < secondVal ? 1 : -1
} else if (firstVal) {
return -1
} else {
return 1
}
if (firstVal && secondVal) {
return firstVal < secondVal ? 1 : -1
}
if (firstVal) {
return -1
}
return 1
},
})
})
// biome-ignore lint/complexity/noForEach: <explanation>
study.union_user_attrs.forEach((attr_spec) => {
columns.push({
field: "user_attrs",
@@ -127,8 +121,7 @@ export const TrialTable: FC<{
?.value || null,
sortable: attr_spec.sortable,
filterable: false,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
less: (firstEl, secondEl, _): number => {
less: (firstEl, secondEl): number => {
const firstVal = firstEl.user_attrs.find(
(attr) => attr.key === attr_spec.key
)?.value
@@ -138,13 +131,14 @@ export const TrialTable: FC<{
if (firstVal === secondVal) {
return 0
} else if (firstVal && secondVal) {
return firstVal < secondVal ? 1 : -1
} else if (firstVal) {
return -1
} else {
return 1
}
if (firstVal && secondVal) {
return firstVal < secondVal ? 1 : -1
}
if (firstVal) {
return -1
}
return 1
},
})
})
-417
View File
@@ -1,417 +0,0 @@
// @ts-ignore
import sqlite3InitModule from "@sqlite.org/sqlite-wasm"
import { SetterOrUpdater } from "recoil"
type SQLite3DB = {
exec(options: {
sql: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (...args: any[]) => void
}): void
}
export const loadSQLite3Storage = (
arrayBuffer: ArrayBuffer,
setter: SetterOrUpdater<Study[]>
): void => {
sqlite3InitModule({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
print: (...args: any): void => {
console.log(args)
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
printErr: (...args: any): void => {
console.log(args)
},
// @ts-ignore
}).then((sqlite3) => {
const p = sqlite3.wasm.allocFromTypedArray(arrayBuffer)
const db = new sqlite3.oo1.DB()
const rc = sqlite3.capi.sqlite3_deserialize(
// @ts-ignore
db.pointer,
"main",
p,
arrayBuffer.byteLength,
arrayBuffer.byteLength,
sqlite3.capi.SQLITE_DESERIALIZE_FREEONCLOSE
)
db.checkRc(rc)
try {
const schemaVersion = getSchemaVersion(db)
if (!isSupportedSchema(schemaVersion)) {
return
}
const studies = getStudies(db, schemaVersion)
setter((prev) => [...prev, ...studies])
} finally {
db.close()
}
})
}
const getSchemaVersion = (db: SQLite3DB): string => {
let schemaVersion = ""
db.exec({
sql: "SELECT version_num FROM alembic_version LIMIT 1",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
schemaVersion = vals[0]
},
})
return schemaVersion
}
const isSupportedSchema = (schemaVersion: string): boolean => {
const lowestVersion = "v2.6.0.a" // supported: "v3.2.0.a", "v3.0.0.{a,b,c,d}", "v2.6.0.a"
if (schemaVersion === lowestVersion) return true
return isGreaterSchemaVersion(schemaVersion, lowestVersion)
}
const isGreaterSchemaVersion = (
leftVersion: string,
rightVersion: string
): boolean => {
// return leftVersion > rightVersion
const leftSuffix = leftVersion.split(".").reverse()[0]
const rightSuffix = rightVersion.split(".").reverse()[0]
leftVersion = leftVersion.replace(/\D/g, "")
rightVersion = rightVersion.replace(/\D/g, "")
const left = Number(leftVersion)
const right = Number(rightVersion)
if (left === right) return leftSuffix > rightSuffix
return left > right
}
const getStudies = (db: SQLite3DB, schemaVersion: string): Study[] => {
const studies: Study[] = []
db.exec({
sql:
"SELECT s.study_id, s.study_name, sd.direction, sd.objective" +
" FROM studies AS s INNER JOIN study_directions AS sd" +
" ON s.study_id = sd.study_id ORDER BY sd.study_direction_id",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
const studyId = vals[0]
const studyName = vals[1]
const direction: StudyDirection =
vals[2] === "MINIMIZE" ? "minimize" : "maximize"
const objective = vals[3]
const trials = getTrials(db, studyId, schemaVersion)
const union_search_space: SearchSpaceItem[] = []
const union_user_attrs: AttributeSpec[] = []
let intersection_search_space: Set<SearchSpaceItem> = new Set()
trials.forEach((trial) => {
const userAttrs = getTrialUserAttributes(db, trial.trial_id)
userAttrs.forEach((attr) => {
if (union_user_attrs.findIndex((s) => s.key === attr.key) === -1) {
union_user_attrs.push({ key: attr.key, sortable: false })
}
})
const params = getTrialParams(db, trial.trial_id)
const param_names = new Set<string>()
params.forEach((param) => {
param_names.add(param.name)
if (
union_search_space.findIndex((s) => s.name === param.name) === -1
) {
union_search_space.push({ name: param.name })
}
})
if (intersection_search_space.size === 0) {
param_names.forEach((s) => {
intersection_search_space.add({
name: s,
})
})
} else {
intersection_search_space = new Set(
Array.from(intersection_search_space).filter((s) =>
param_names.has(s.name)
)
)
}
trial.params = params
trial.user_attrs = userAttrs
})
if (objective === 0) {
studies.push({
study_id: studyId,
study_name: studyName,
directions: [direction],
union_search_space: union_search_space,
intersection_search_space: Array.from(intersection_search_space),
union_user_attrs: union_user_attrs,
trials: trials,
})
return
}
const index = studies.findIndex((s) => s.study_id === studyId)
studies[index].directions.push(direction)
},
})
return studies
}
const getTrials = (
db: SQLite3DB,
studyId: number,
schemaVersion: string
): Trial[] => {
const trials: Trial[] = []
db.exec({
sql:
"SELECT trial_id, number, state, datetime_start, datetime_complete FROM trials" +
` WHERE study_id = ${studyId} ORDER BY number`,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
const trialId = vals[0]
const state: TrialState =
vals[2] === "COMPLETE"
? "Complete"
: vals[2] === "PRUNED"
? "Pruned"
: vals[2] === "RUNNING"
? "Running"
: vals[2] === "WAITING"
? "Waiting"
: "Fail"
const trial: Trial = {
trial_id: trialId,
number: vals[1],
study_id: studyId,
state: state,
values: getTrialValues(db, trialId, schemaVersion),
intermediate_values: getTrialIntermediateValues(
db,
trialId,
schemaVersion
),
params: [], // Set this column later
user_attrs: [], // Set this column later
datetime_start: vals[3],
datetime_complete: vals[4],
}
trials.push(trial)
},
})
return trials
}
const getTrialValues = (
db: SQLite3DB,
trialId: number,
schemaVersion: string
): TrialValueNumber[] => {
const values: TrialValueNumber[] = []
if (isGreaterSchemaVersion(schemaVersion, "v3.0.0.c")) {
db.exec({
sql:
"SELECT value, value_type" +
` FROM trial_values WHERE trial_id = ${trialId}` +
" ORDER BY objective",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
values.push(
vals[1] === "INF_NEG"
? "-inf"
: vals[1] === "INF_POS"
? "+inf"
: vals[0]
)
},
})
} else {
db.exec({
sql:
"SELECT value" +
` FROM trial_values WHERE trial_id = ${trialId}` +
" ORDER BY objective",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
values.push(vals[0])
},
})
}
return values
}
const getTrialParams = (db: SQLite3DB, trialId: number): TrialParam[] => {
const params: TrialParam[] = []
db.exec({
sql:
"SELECT param_name, param_value, distribution_json" +
` FROM trial_params WHERE trial_id = ${trialId}`,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
const distribution = parseDistributionJSON(vals[2])
params.push({
name: vals[0],
param_internal_value: vals[1],
param_external_type: distribution.type,
param_external_value: paramInternalValueToExternalValue(
distribution,
vals[1]
),
distribution: distribution,
})
},
})
return params
}
const paramInternalValueToExternalValue = (
distribution: Distribution,
internalValue: number
): string => {
if (distribution.type === "FloatDistribution") {
return internalValue.toString()
} else if (distribution.type === "IntDistribution") {
return internalValue.toString()
} else {
return distribution.choices[internalValue].value
}
}
const parseDistributionJSON = (t: string): Distribution => {
const parsed = JSON.parse(t)
if (parsed.name === "FloatDistribution") {
return {
type: "FloatDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: parsed.attributes.step as number,
log: parsed.attributes.log as boolean,
}
} else if (parsed.name === "UniformDistribution") {
return {
type: "FloatDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: null,
log: false,
}
} else if (parsed.name === "LogUniformDistribution") {
return {
type: "FloatDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: null,
log: true,
}
} else if (parsed.name === "DiscreteUniformDistribution") {
return {
type: "FloatDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: parsed.attributes.q,
log: false,
}
} else if (parsed.name === "IntDistribution") {
return {
type: "IntDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: parsed.attributes.step as number,
log: parsed.attributes.log as boolean,
}
} else if (parsed.name === "IntUniformDistribution") {
return {
type: "IntDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: parsed.attributes.step as number,
log: false,
}
} else if (parsed.name === "IntLogUniformDistribution") {
return {
type: "IntDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: parsed.attributes.step as number,
log: true,
}
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const choices = parsed.attributes.choices.map((value: any) => {
// TODO(c-bata): Support other types
return {
pytype: "str",
value: value.toString(),
}
})
return {
type: "CategoricalDistribution",
choices: choices,
}
}
}
const getTrialUserAttributes = (
db: SQLite3DB,
trialId: number
): Attribute[] => {
const attrs: Attribute[] = []
db.exec({
sql:
"SELECT key, value_json" +
` FROM trial_user_attributes WHERE trial_id = ${trialId}`,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
attrs.push({
key: vals[0],
value: vals[1],
})
},
})
return attrs
}
const getTrialIntermediateValues = (
db: SQLite3DB,
trialId: number,
schemaVersion: string
): TrialIntermediateValue[] => {
const values: TrialIntermediateValue[] = []
if (isGreaterSchemaVersion(schemaVersion, "v3.0.0.c")) {
db.exec({
sql:
"SELECT step, intermediate_value, intermediate_value_type" +
` FROM trial_intermediate_values WHERE trial_id = ${trialId}` +
" ORDER BY step",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
values.push({
step: vals[0],
value:
vals[2] === "INF_NEG"
? "-inf"
: vals[2] === "INF_POS"
? "+inf"
: vals[2] === "NAN"
? "nan"
: vals[1],
})
},
})
} else {
db.exec({
sql:
"SELECT step, intermediate_value" +
` FROM trial_intermediate_values WHERE trial_id = ${trialId}` +
" ORDER BY step",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
values.push({
step: vals[0],
value: vals[1],
})
},
})
}
return values
}
-6
View File
@@ -1,6 +0,0 @@
import { atom } from "recoil"
export const studiesState = atom<Study[]>({
key: "studies",
default: [],
})
+16 -6
View File
@@ -1,11 +1,14 @@
declare const IS_VSCODE: boolean
type TrialValueNumber = number | "inf" | "-inf"
type TrialIntermediateValueNumber = number | "inf" | "-inf" | "nan"
type TrialState = "Running" | "Complete" | "Pruned" | "Fail" | "Waiting"
type TrialStateFinished = "Complete" | "Fail" | "Pruned"
type StudyDirection = "maximize" | "minimize" | "not_set"
type OptunaStorage = {
getStudies: () => Promise<StudySummary[]>
getStudy: (idx: number) => Promise<Study | null>
}
type FloatDistribution = {
type: "FloatDistribution"
low: number
@@ -22,14 +25,15 @@ type IntDistribution = {
log: boolean
}
type CategoricalChoiceType = null | boolean | number | string
type CategoricalDistribution = {
type: "CategoricalDistribution"
choices: { pytype: string; value: string }[]
choices: CategoricalChoiceType[]
}
type TrialIntermediateValue = {
step: number
value: TrialIntermediateValueNumber
value: number
}
type Distribution =
@@ -47,6 +51,12 @@ type AttributeSpec = {
sortable: boolean
}
type StudySummary = {
study_id: number
study_name: string
directions: StudyDirection[]
}
type Study = {
study_id: number
study_name: string
@@ -63,7 +73,7 @@ type Trial = {
number: number
study_id: number
state: TrialState
values?: TrialValueNumber[]
values?: number[]
params: TrialParam[]
intermediate_values: TrialIntermediateValue[]
user_attrs: Attribute[]
@@ -74,7 +84,7 @@ type Trial = {
type TrialParam = {
name: string
param_internal_value: number
param_external_value: string
param_external_value: CategoricalChoiceType
param_external_type: string
distribution: Distribution
}
+12 -25
View File
@@ -1,22 +1,17 @@
import React, { FC, useEffect } from "react"
import React, { FC, useEffect, useContext } from "react"
import ReactDOM from "react-dom/client"
import "./index.css"
import { App } from "./components/App"
import { RecoilRoot, useSetRecoilState, SetterOrUpdater } from "recoil"
import { studiesState } from "./state"
import { loadSQLite3Storage } from "./sqlite3"
import { loadJournalStorage } from "./journalStorage"
import {
StorageContext,
StorageProvider,
getStorage,
} from "./components/StorageProvider"
import "./index.css"
export const AppWrapper: FC = () => {
const setStudies = useSetRecoilState<Study[]>(studiesState)
const onceSetStudies: SetterOrUpdater<Study[]> = (
setter: (currVal: Study[]) => Study[]
): void => {
const studies = setter([])
setStudies(studies)
}
const { setStorage } = useContext(StorageContext)
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
window.addEventListener("message", (event) => {
const message = event.data
@@ -25,8 +20,6 @@ export const AppWrapper: FC = () => {
let len: number
let bytes: Uint8Array
let arrayBuffer: ArrayBuffer
let header: Uint8Array
let headerString: string
switch (message.type) {
case "optunaStorage":
@@ -38,13 +31,7 @@ export const AppWrapper: FC = () => {
bytes[i] = binaryString.charCodeAt(i)
}
arrayBuffer = bytes.buffer
header = new Uint8Array(arrayBuffer, 0, 16)
headerString = new TextDecoder().decode(header)
if (headerString === "SQLite format 3\u0000") {
loadSQLite3Storage(arrayBuffer, onceSetStudies)
} else {
loadJournalStorage(arrayBuffer, onceSetStudies)
}
setStorage(getStorage(arrayBuffer))
break
}
})
@@ -54,8 +41,8 @@ export const AppWrapper: FC = () => {
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RecoilRoot>
<StorageProvider>
<AppWrapper />
</RecoilRoot>
</StorageProvider>
</React.StrictMode>
)
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
define: {
'IS_VSCODE': JSON.stringify(false),
},
optimizeDeps: {
exclude: ['@sqlite.org/sqlite-wasm'],
},
server: {
fs: {
// Allow serving wasm files in node_modules
allow: ['..'],
},
},
});
+40 -93
View File
@@ -1,101 +1,48 @@
const webpack = require('webpack');
const path = require('path');
const CompressionPlugin = require("compression-webpack-plugin");
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
const isDev = mode === 'development';
const PUBLIC_PATH = process.env.PUBLIC_PATH ?? '/public/';
if (!PUBLIC_PATH.endsWith('/')) {
PUBLIC_PATH += '/';
}
const typeScriptLoader = process.env.TYPESCRIPT_LOADER === "esbuild-loader" ? {
test: /\.tsx?$/,
exclude: [/node_modules/],
loader: 'esbuild-loader',
options: {
loader: 'tsx',
tsconfigRaw: require('./tsconfig.json')
}
} : {
test: /\.tsx?$/,
exclude: [/node_modules/],
loader: 'ts-loader',
options: {
configFile: __dirname + '/tsconfig.json',
transpileOnly: isDev,
happyPackMode: true
}
}
var config = [
{
mode,
experiments: {
syncWebAssembly: true,
asyncWebAssembly: true,
},
entry: [__dirname + '/src/browser_app_entry.tsx'],
output: {
path: __dirname + '/public/',
filename: 'bundle.js',
publicPath: PUBLIC_PATH
},
module: {
rules: [{ oneOf: [typeScriptLoader] }]
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
plugins: [
new webpack.DefinePlugin({'IS_VSCODE': JSON.stringify(false)})
]
module.exports = {
mode: "production",
devtool: 'source-map',
experiments: {
syncWebAssembly: true,
asyncWebAssembly: true,
},
{
mode,
experiments: {
syncWebAssembly: true,
asyncWebAssembly: true,
},
entry: [__dirname + '/src/vscode_entry.tsx'],
output: {
path: path.resolve(__dirname, '../vscode/assets/'),
filename: 'bundle.js',
publicPath: '/'
},
module: {
rules: [
{ oneOf: [typeScriptLoader] },
{
test: /\.wasm$/,
type: "asset/inline",
},
]
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
plugins: [
new webpack.DefinePlugin({'IS_VSCODE': JSON.stringify(true)})
]
},
];
if (isDev) {
config[0].devtool = 'source-map';
config[0].cache = {
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename],
}
}
console.log('= = = = = = = = = = = = = = = = = = =');
console.log('DEVELOPMENT BUILD');
console.log(process.env.TYPESCRIPT_LOADER === 'esbuild-loader' ? 'esbuild-loader' : 'ts-loader');
console.log('= = = = = = = = = = = = = = = = = = =');
} else {
const CompressionPlugin = require("compression-webpack-plugin");
config[0].plugins.push(new CompressionPlugin())
config[1].plugins.push(new CompressionPlugin())
}
module.exports = config;
},
entry: [__dirname + '/src/vscode_entry.tsx'],
output: {
path: path.resolve(__dirname, '../vscode/assets/'),
filename: 'bundle.js',
publicPath: '/'
},
module: {
rules: [
{ oneOf: [{
test: /\.tsx?$/,
exclude: [/node_modules/],
loader: 'esbuild-loader',
options: {
loader: 'tsx',
tsconfigRaw: require('./tsconfig.json')
}
}] },
{
test: /\.wasm$/,
type: "asset/inline",
},
]
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
plugins: [
new webpack.DefinePlugin({ 'IS_VSCODE': JSON.stringify(true) }),
new CompressionPlugin()
]
};
+6
View File
@@ -0,0 +1,6 @@
{
"name": "tslib",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@optuna/storage",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@optuna/storage",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@sqlite.org/sqlite-wasm": "^3.45.1-build1"
},
"devDependencies": {
"@optuna/types": "../types/"
}
},
"../entity": {
"name": "@optuna/entity",
"version": "0.0.1",
"extraneous": true,
"license": "MIT"
},
"../types": {
"name": "@optuna/types",
"version": "0.0.1",
"dev": true,
"license": "MIT"
},
"node_modules/@optuna/types": {
"resolved": "../types",
"link": true
},
"node_modules/@sqlite.org/sqlite-wasm": {
"version": "3.45.1-build1",
"resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.45.1-build1.tgz",
"integrity": "sha512-1EgshFNhVeBtZ9KtQPm3PzzJ2CtpmXAq2DAPywy7WZ3gOK6p5n8TY+M+mBMpQCF5cLqrdNFb3Kp9uNie9rUAHw==",
"bin": {
"sqlite-wasm": "bin/index.js"
}
}
}
}
+31
View File
@@ -0,0 +1,31 @@
{
"name": "@optuna/storage",
"type": "module",
"private": true,
"version": "0.0.1",
"description": "Loaders for Optuna storages",
"main": "pkg/index.js",
"scripts": {
"build": "tsc -d",
"test": "node --test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/optuna/optuna-dashboard.git"
},
"keywords": [
"optuna"
],
"author": "Optuna Development Team",
"license": "MIT",
"bugs": {
"url": "https://github.com/optuna/optuna-dashboard/issues"
},
"homepage": "https://github.com/optuna/optuna-dashboard#readme",
"dependencies": {
"@sqlite.org/sqlite-wasm": "^3.45.1-build1"
},
"devDependencies": {
"@optuna/types": "../types/"
}
}
+2
View File
@@ -0,0 +1,2 @@
export { JournalFileStorage } from "./journal"
export { SQLite3Storage } from "./sqlite"
@@ -1,4 +1,5 @@
import { SetterOrUpdater } from "recoil"
import * as Optuna from "@optuna/types"
import { OptunaStorage } from "./storage"
// JournalStorage
enum JournalOperation {
@@ -33,9 +34,12 @@ interface JournalOpCreateTrial extends JournalOpBase {
datetime_start?: string
datetime_complete?: string
distributions?: { [key: string]: string }
params?: { [key: string]: any } // eslint-disable-line @typescript-eslint/no-explicit-any
user_attrs?: { [key: string]: any } // eslint-disable-line @typescript-eslint/no-explicit-any
system_attrs?: { [key: string]: any } // eslint-disable-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
params?: { [key: string]: any }
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
user_attrs?: { [key: string]: any }
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
system_attrs?: { [key: string]: any }
state?: number
intermediate_values?: { [key: string]: number }
value?: number
@@ -65,10 +69,11 @@ interface JournalOpSetTrialIntermediateValue extends JournalOpBase {
interface JournalOpSetTrialUserAttr extends JournalOpBase {
trial_id: number
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
user_attr: { [key: string]: any } // eslint-disable-line @typescript-eslint/no-explicit-any
}
const trialStateNumToTrialState = (state: number): TrialState => {
const trialStateNumToTrialState = (state: number): Optuna.TrialState => {
switch (state) {
case 0:
return "Running"
@@ -85,41 +90,37 @@ const trialStateNumToTrialState = (state: number): TrialState => {
}
}
const parseDistribution = (distribution: string): Distribution => {
const parseDistribution = (distribution: string): Optuna.Distribution => {
const distributionJson = JSON.parse(distribution)
if (distributionJson["name"] === "IntDistribution") {
if (distributionJson.name === "IntDistribution") {
return {
...distributionJson["attributes"],
...distributionJson.attributes,
type: "IntDistribution",
}
} else if (distributionJson["name"] === "FloatDistribution") {
}
if (distributionJson.name === "FloatDistribution") {
return {
...distributionJson["attributes"],
...distributionJson.attributes,
type: "FloatDistribution",
}
} else {
}
if (distributionJson.name === "CategoricalDistribution") {
return {
// TODO(gen740): support other types
type: "CategoricalDistribution",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
choices: distributionJson["attributes"]["choices"].map((choice: any) => {
return {
pytype: "str",
value: choice.toString(),
}
}),
choices: distributionJson.attributes.choices,
}
}
throw new Error(`Unexpected distribution: ${distribution}`)
}
class JournalStorage {
private studies: Study[] = []
private studies: Optuna.Study[] = []
private nextStudyId = 0
private studyIdToTrialIDs: Map<number, number[]> = new Map()
private trialIdToStudyId: Map<number, number> = new Map()
private trialID = 0
public getStudies(): Study[] {
public getStudies(): Optuna.Study[] {
for (const study of this.studies) {
const unionUserAttrs: Set<string> = new Set()
const unionSearchSpace: Set<string> = new Set()
@@ -186,11 +187,13 @@ class JournalStorage {
return
}
const params: TrialParam[] =
const params: Optuna.TrialParam[] =
log.params === undefined || log.distributions === undefined
? []
: Object.entries(log.params).map(([name, value]) => {
const distribution = parseDistribution(log.distributions![name])
const distribution = parseDistribution(
log.distributions?.[name] || ""
)
return {
name: name,
param_internal_value: value,
@@ -198,11 +201,11 @@ class JournalStorage {
param_external_value: (() => {
if (distribution.type === "FloatDistribution") {
return value.toString()
} else if (distribution.type === "IntDistribution") {
return value.toString()
} else {
return distribution.choices[value].value
}
if (distribution.type === "IntDistribution") {
return value.toString()
}
return distribution.choices[value]
})(),
distribution: distribution,
}
@@ -225,11 +228,11 @@ class JournalStorage {
values: (() => {
if (log.value !== undefined) {
return [log.value]
} else if (log.values !== undefined) {
return log.values
} else {
return undefined
}
if (log.values !== undefined) {
return log.values
}
return undefined
})(),
params: params,
intermediate_values: [],
@@ -251,7 +254,7 @@ class JournalStorage {
this.trialID++
}
private getStudyAndTrial(trial_id: number): [Study?, Trial?] {
private getStudyAndTrial(trial_id: number): [Optuna.Study?, Optuna.Trial?] {
const study = this.studies.find(
(item) => item.study_id === this.trialIdToStudyId.get(trial_id)
)
@@ -327,10 +330,7 @@ class JournalStorage {
}
}
export const loadJournalStorage = (
arrayBuffer: ArrayBuffer,
setter: SetterOrUpdater<Study[]>
): void => {
const loadJournalStorage = (arrayBuffer: ArrayBuffer): Optuna.Study[] => {
const decoder = new TextDecoder("utf-8")
const logs = decoder.decode(arrayBuffer).split("\n")
@@ -381,6 +381,18 @@ export const loadJournalStorage = (
}
}
const studies = journalStorage.getStudies()
setter((prev) => [...prev, ...studies])
return journalStorage.getStudies()
}
export class JournalFileStorage implements OptunaStorage {
studies: Optuna.Study[]
constructor(arrayBuffer: ArrayBuffer) {
this.studies = loadJournalStorage(arrayBuffer)
}
getStudies = async (): Promise<Optuna.StudySummary[]> => {
return this.studies
}
getStudy = async (idx: number): Promise<Optuna.Study | null> => {
return this.studies[idx] || null
}
}
+425
View File
@@ -0,0 +1,425 @@
import * as Optuna from "@optuna/types"
// @ts-ignore
import sqlite3InitModule from "@sqlite.org/sqlite-wasm"
import { OptunaStorage } from "./storage"
type SQLite3DB = {
exec(options: {
sql: string
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
callback: (...args: any[]) => void
}): void
close(): void
}
export class SQLite3Storage implements OptunaStorage {
db: Promise<SQLite3DB>
summaries_cache: Optuna.StudySummary[] | null
constructor(arrayBuffer: ArrayBuffer) {
this.db = this.initDB(arrayBuffer)
this.summaries_cache = null
}
async initDB(arrayBuffer: ArrayBuffer): Promise<SQLite3DB> {
return sqlite3InitModule({
print: console.log,
printErr: console.log,
// @ts-ignore
}).then((sqlite3) => {
const p = sqlite3.wasm.allocFromTypedArray(arrayBuffer)
const db = new sqlite3.oo1.DB()
const rc = sqlite3.capi.sqlite3_deserialize(
// @ts-ignore
db.pointer,
"main",
p,
arrayBuffer.byteLength,
arrayBuffer.byteLength,
sqlite3.capi.SQLITE_DESERIALIZE_FREEONCLOSE
)
db.checkRc(rc)
return db
})
}
getStudies = async (): Promise<Optuna.StudySummary[]> => {
const db = await this.db
this.summaries_cache = getStudySummaries(db)
return this.summaries_cache
}
getStudy = async (idx: number): Promise<Optuna.Study | null> => {
const db = await this.db
const schemaVersion = getSchemaVersion(db)
if (!isSupportedSchema(schemaVersion)) {
return null
}
if (this.summaries_cache === null) {
this.summaries_cache = getStudySummaries(db)
}
const summary = this.summaries_cache[idx]
if (summary === undefined) {
return null
}
return getStudy(db, schemaVersion, summary)
}
}
const getSchemaVersion = (db: SQLite3DB): string => {
let schemaVersion = ""
db.exec({
sql: "SELECT version_num FROM alembic_version LIMIT 1",
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
callback: (vals: any[]) => {
schemaVersion = vals[0]
},
})
return schemaVersion
}
const isSupportedSchema = (schemaVersion: string): boolean => {
const lowestVersion = "v2.6.0.a" // supported: "v3.2.0.a", "v3.0.0.{a,b,c,d}", "v2.6.0.a"
if (schemaVersion === lowestVersion) return true
return isGreaterSchemaVersion(schemaVersion, lowestVersion)
}
const isGreaterSchemaVersion = (
leftVersion: string,
rightVersion: string
): boolean => {
// return leftVersion > rightVersion
const leftSuffix = leftVersion.split(".").reverse()[0]
const rightSuffix = rightVersion.split(".").reverse()[0]
const leftVersion_ = leftVersion.replace(/\D/g, "")
const rightVersion_ = rightVersion.replace(/\D/g, "")
const left = Number(leftVersion_)
const right = Number(rightVersion_)
if (left === right) return leftSuffix > rightSuffix
return left > right
}
const getStudySummaries = (db: SQLite3DB): Optuna.StudySummary[] => {
const summaries: Optuna.StudySummary[] = []
db.exec({
sql:
"SELECT s.study_id, s.study_name, sd.direction, sd.objective" +
" FROM studies AS s INNER JOIN study_directions AS sd" +
" ON s.study_id = sd.study_id ORDER BY sd.study_direction_id",
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
callback: (vals: any[]) => {
const studyId = vals[0]
const studyName = vals[1]
const direction: Optuna.StudyDirection =
vals[2] === "MINIMIZE" ? "minimize" : "maximize"
const objective = vals[3]
if (objective === 0) {
summaries.push({
study_id: studyId,
study_name: studyName,
directions: [direction],
})
return
}
const index = summaries.findIndex((s) => s.study_id === studyId)
summaries[index].directions.push(direction)
},
})
return summaries
}
const getStudy = (
db: SQLite3DB,
schemaVersion: string,
summary: Optuna.StudySummary
): Optuna.Study => {
const study: Optuna.Study = {
study_id: summary.study_id,
study_name: summary.study_name,
directions: summary.directions,
union_search_space: [],
intersection_search_space: [],
union_user_attrs: [],
trials: [],
}
let intersection_search_space: Set<Optuna.SearchSpaceItem> = new Set()
study.trials = getTrials(db, summary.study_id, schemaVersion)
for (const trial of study.trials) {
const userAttrs = getTrialUserAttributes(db, trial.trial_id)
for (const attr of userAttrs) {
if (study.union_user_attrs.findIndex((s) => s.key === attr.key) === -1) {
study.union_user_attrs.push({ key: attr.key, sortable: false })
}
}
const params = getTrialParams(db, trial.trial_id)
const param_names = new Set<string>()
for (const param of params) {
param_names.add(param.name)
if (
study.union_search_space.findIndex((s) => s.name === param.name) === -1
) {
study.union_search_space.push({ name: param.name })
}
}
if (intersection_search_space.size === 0) {
// biome-ignore lint/complexity/noForEach: <explanation>
param_names.forEach((s) => {
intersection_search_space.add({ name: s })
})
} else {
intersection_search_space = new Set(
Array.from(intersection_search_space).filter((s) =>
param_names.has(s.name)
)
)
}
trial.params = params
trial.user_attrs = userAttrs
}
study.intersection_search_space = Array.from(intersection_search_space)
return study
}
const getTrials = (
db: SQLite3DB,
studyId: number,
schemaVersion: string
): Optuna.Trial[] => {
const trials: Optuna.Trial[] = []
db.exec({
sql: `SELECT trial_id, number, state, datetime_start, datetime_complete FROM trials WHERE study_id = ${studyId} ORDER BY number`,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
callback: (vals: any[]) => {
const trialId = vals[0]
const state: Optuna.TrialState =
vals[2] === "COMPLETE"
? "Complete"
: vals[2] === "PRUNED"
? "Pruned"
: vals[2] === "RUNNING"
? "Running"
: vals[2] === "WAITING"
? "Waiting"
: "Fail"
const trial: Optuna.Trial = {
trial_id: trialId,
number: vals[1],
study_id: studyId,
state: state,
values: getTrialValues(db, trialId, schemaVersion),
intermediate_values: getTrialIntermediateValues(
db,
trialId,
schemaVersion
),
params: [], // Set this column later
user_attrs: [], // Set this column later
datetime_start: vals[3],
datetime_complete: vals[4],
}
trials.push(trial)
},
})
return trials
}
const getTrialValues = (
db: SQLite3DB,
trialId: number,
schemaVersion: string
): number[] => {
const values: number[] = []
if (isGreaterSchemaVersion(schemaVersion, "v3.0.0.c")) {
db.exec({
sql: `SELECT value, value_type FROM trial_values WHERE trial_id = ${trialId} ORDER BY objective`,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
callback: (vals: any[]) => {
values.push(
vals[1] === "INF_NEG"
? -Infinity
: vals[1] === "INF_POS"
? Infinity
: vals[0]
)
},
})
} else {
db.exec({
sql: `SELECT value FROM trial_values WHERE trial_id = ${trialId} ORDER BY objective`,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
callback: (vals: any[]) => {
values.push(vals[0])
},
})
}
return values
}
const getTrialParams = (
db: SQLite3DB,
trialId: number
): Optuna.TrialParam[] => {
const params: Optuna.TrialParam[] = []
db.exec({
sql: `SELECT param_name, param_value, distribution_json FROM trial_params WHERE trial_id = ${trialId}`,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
callback: (vals: any[]) => {
const distribution = parseDistributionJSON(vals[2])
params.push({
name: vals[0],
param_internal_value: vals[1],
param_external_type: distribution.type,
param_external_value: paramInternalValueToExternalValue(
distribution,
vals[1]
),
distribution: distribution,
})
},
})
return params
}
const paramInternalValueToExternalValue = (
distribution: Optuna.Distribution,
internalValue: number
): Optuna.CategoricalChoiceType => {
if (distribution.type === "FloatDistribution") {
return internalValue.toString()
}
if (distribution.type === "IntDistribution") {
return internalValue.toString()
}
return distribution.choices[internalValue]
}
const parseDistributionJSON = (t: string): Optuna.Distribution => {
const parsed = JSON.parse(t)
if (parsed.name === "FloatDistribution") {
return {
type: "FloatDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: parsed.attributes.step as number,
log: parsed.attributes.log as boolean,
}
}
if (parsed.name === "UniformDistribution") {
return {
type: "FloatDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: null,
log: false,
}
}
if (parsed.name === "LogUniformDistribution") {
return {
type: "FloatDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: null,
log: true,
}
}
if (parsed.name === "DiscreteUniformDistribution") {
return {
type: "FloatDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: parsed.attributes.q,
log: false,
}
}
if (parsed.name === "IntDistribution") {
return {
type: "IntDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: parsed.attributes.step as number,
log: parsed.attributes.log as boolean,
}
}
if (parsed.name === "IntUniformDistribution") {
return {
type: "IntDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: parsed.attributes.step as number,
log: false,
}
}
if (parsed.name === "IntLogUniformDistribution") {
return {
type: "IntDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: parsed.attributes.step as number,
log: true,
}
}
return {
type: "CategoricalDistribution",
choices: parsed.attributes.choices,
}
}
const getTrialUserAttributes = (
db: SQLite3DB,
trialId: number
): Optuna.Attribute[] => {
const attrs: Optuna.Attribute[] = []
db.exec({
sql: `SELECT key, value_json FROM trial_user_attributes WHERE trial_id = ${trialId}`,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
callback: (vals: any[]) => {
attrs.push({
key: vals[0],
value: vals[1],
})
},
})
return attrs
}
const getTrialIntermediateValues = (
db: SQLite3DB,
trialId: number,
schemaVersion: string
): Optuna.TrialIntermediateValue[] => {
const values: Optuna.TrialIntermediateValue[] = []
if (isGreaterSchemaVersion(schemaVersion, "v3.0.0.c")) {
db.exec({
sql: `SELECT step, intermediate_value, intermediate_value_type FROM trial_intermediate_values WHERE trial_id = ${trialId} ORDER BY step`,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
callback: (vals: any[]) => {
values.push({
step: vals[0],
value:
vals[2] === "INF_NEG"
? -Infinity
: vals[2] === "INF_POS"
? Infinity
: vals[2] === "NAN"
? NaN
: vals[1],
})
},
})
} else {
db.exec({
sql: `SELECT step, intermediate_value FROM trial_intermediate_values WHERE trial_id = ${trialId} ORDER BY step`,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
callback: (vals: any[]) => {
values.push({
step: vals[0],
value: vals[1],
})
},
})
}
return values
}
+6
View File
@@ -0,0 +1,6 @@
import * as Optuna from "@optuna/types"
export type OptunaStorage = {
getStudies: () => Promise<Optuna.StudySummary[]>
getStudy: (idx: number) => Promise<Optuna.Study | null>
}
+58
View File
@@ -0,0 +1,58 @@
import os.path
import shutil
import optuna
from optuna.storages import BaseStorage
from optuna.storages import JournalFileStorage
from optuna.storages import JournalStorage
from optuna.storages import RDBStorage
optuna.logging.set_verbosity(optuna.logging.WARNING)
BASE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "asset")
def remove_assets() -> None:
if os.path.exists(BASE_DIR):
shutil.rmtree(BASE_DIR)
os.mkdir(BASE_DIR)
def create_optuna_storage(storage: BaseStorage) -> None:
# Single-objective study
study = optuna.create_study(
study_name="single-objective", storage=storage, sampler=optuna.samplers.RandomSampler()
)
print(f"Generating {study.study_name} for {type(storage).__name__}...")
def objective_single(trial: optuna.Trial) -> float:
x1 = trial.suggest_float("x1", 0, 10)
x2 = trial.suggest_float("x2", 0, 10)
trial.suggest_categorical("x3", ["foo", "bar"])
return (x1 - 2) ** 2 + (x2 - 5) ** 2
study.optimize(objective_single, n_trials=50)
# Single-objective study with dynamic search space
study = optuna.create_study(
study_name="single-objective-dynamic", storage=storage, direction="maximize"
)
print(f"Generating {study.study_name} for {type(storage).__name__}...")
def objective_single_dynamic(trial: optuna.Trial) -> float:
category = trial.suggest_categorical("category", ["foo", "bar"])
if category == "foo":
return (trial.suggest_float("x1", 0, 10) - 2) ** 2
else:
return -((trial.suggest_float("x2", -10, 0) + 5) ** 2)
study.optimize(objective_single_dynamic, n_trials=50)
if __name__ == "__main__":
remove_assets()
for storage in [
JournalStorage(JournalFileStorage(os.path.join(BASE_DIR, "journal.log"))),
RDBStorage("sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3")),
]:
create_optuna_storage(storage)
+19
View File
@@ -0,0 +1,19 @@
import assert from "node:assert"
import { openAsBlob } from "node:fs"
import path from "node:path"
import test from "node:test"
import * as mut from "../pkg/journal.js"
const n_studies = 2
test("Test Journal File Storage", async () => {
const blob = await openAsBlob(
path.resolve(".", "test", "asset", "journal.log")
)
const buf = await blob.arrayBuffer()
const storage = new mut.JournalFileStorage(buf)
const studies = await storage.getStudies()
assert.strictEqual(studies.length, n_studies)
})
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"isolatedModules": true,
"skipLibCheck": true,
"strictNullChecks": true,
"moduleResolution": "node",
"noUnusedLocals": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noImplicitAny": true,
"lib": ["dom", "esnext"],
"module": "esnext",
"esModuleInterop": true,
"rootDir": "./src",
"outDir": "./pkg",
"allowSyntheticDefaultImports": true,
"target": "es2021",
"sourceMap": true,
"declaration": true,
"strict": true
},
"include": [
"src/**/*.ts"
],
"types": ["node"]
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "@optuna/types",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@optuna/types",
"version": "0.0.1",
"license": "MIT"
}
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@optuna/types",
"private": true,
"version": "0.0.1",
"description": "Entites for Optuna Dashboard",
"main": "pkg/index.js",
"scripts": {
"build": "tsc -d"
},
"repository": {
"type": "git",
"url": "git+https://github.com/optuna/optuna-dashboard.git"
},
"keywords": [
"optuna"
],
"author": "Optuna Development Team",
"license": "MIT",
"bugs": {
"url": "https://github.com/optuna/optuna-dashboard/issues"
},
"homepage": "https://github.com/optuna/optuna-dashboard#readme"
}
+93
View File
@@ -0,0 +1,93 @@
export type TrialState = "Running" | "Complete" | "Pruned" | "Fail" | "Waiting"
export type TrialStateFinished = "Complete" | "Fail" | "Pruned"
export type StudyDirection = "maximize" | "minimize"
export type FloatDistribution = {
type: "FloatDistribution"
low: number
high: number
step: number | null
log: boolean
}
export type IntDistribution = {
type: "IntDistribution"
low: number
high: number
step: number | null
log: boolean
}
export type CategoricalChoiceType = null | boolean | number | string
export type CategoricalDistribution = {
type: "CategoricalDistribution"
choices: CategoricalChoiceType[]
}
export type TrialIntermediateValue = {
step: number
value: number
}
export type Distribution =
| FloatDistribution
| IntDistribution
| CategoricalDistribution
export type Attribute = {
key: string
value: string
}
export type AttributeSpec = {
key: string
sortable: boolean
}
export type StudySummary = {
study_id: number
study_name: string
directions: StudyDirection[]
}
export type Study = {
study_id: number
study_name: string
directions: StudyDirection[]
union_search_space: SearchSpaceItem[]
intersection_search_space: SearchSpaceItem[]
union_user_attrs: AttributeSpec[]
datetime_start?: Date
trials: Trial[]
}
export type Trial = {
trial_id: number
number: number
study_id: number
state: TrialState
values?: number[]
params: TrialParam[]
intermediate_values: TrialIntermediateValue[]
user_attrs: Attribute[]
datetime_start?: Date
datetime_complete?: Date
}
export type TrialParam = {
name: string
param_internal_value: number
param_external_value: CategoricalChoiceType
param_external_type: string
distribution: Distribution
}
export type SearchSpaceItem = {
name: string
}
export type ParamImportance = {
name: string
importance: number
}
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"isolatedModules": true,
"skipLibCheck": true,
"strictNullChecks": true,
"moduleResolution": "node",
"noUnusedLocals": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noImplicitAny": true,
"lib": ["dom", "esnext"],
"module": "esnext",
"esModuleInterop": true,
"rootDir": "./src",
"outDir": "./pkg",
"allowSyntheticDefaultImports": true,
"target": "es5",
"sourceMap": true,
"declaration": true,
"strict": true
},
"include": [
"src/**/*.ts"
],
"types": ["node"]
}
+18 -1
View File
@@ -6,4 +6,21 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
## [Unreleased]
- Initial release
## 0.3.0
* [bug] Fix a bug that the extension cannot show studies on the first attempt.
* [feature] Enable a "Open in Optuna Dashboard" menu for `.log` files.
* [enhancement] Add a slight performance improvement when loading a SQLite3 file that contains a lot of trials.
## 0.2.0
* [feature] Added support for `JournalFileStorage`.
* [feature] Added support for code-server.
## 0.1.0
* [feature] Added older database schemas support (Optuna 2.6.0 or later)
## 0.0.1
* Initial Release
-15
View File
@@ -12,21 +12,6 @@ Please right-click on the SQLite3 file (`*.db` or `*.sqlite3`) in the file explo
Nothing to configure.
## Release Notes
### 0.2.0
* Added support for `JournalFileStorage`.
* Added support for code-server.
### 0.1.0
Added older database schemas support (Optuna 2.6.0 or later)
### 0.0.1
Initial Release
---
**Enjoy!**
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "optuna-dashboard",
"version": "0.2.0",
"version": "0.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "optuna-dashboard",
"version": "0.2.0",
"version": "0.3.0",
"license": "MIT",
"devDependencies": {
"@types/mocha": "^10.0.1",
+1 -1
View File
@@ -3,7 +3,7 @@
"displayName": "Optuna Dashboard",
"description": "Web Dashboard for Optuna",
"publisher": "Optuna",
"version": "0.2.0",
"version": "0.3.0",
"license": "MIT",
"icon": "images/optuna-logo.png",
"engines": {

Some files were not shown because too many files have changed in this diff Show More