mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-10 12:23:22 +08:00
Merge branch 'main' of github.com:porink0424/optuna-dashboard into feat/tslib
This commit is contained in:
+5
-2
@@ -1,2 +1,5 @@
|
||||
venv/**/*.ts
|
||||
venv/**/*.js
|
||||
venv
|
||||
.venv
|
||||
standalone_app
|
||||
vscode
|
||||
tslib
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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 .
|
||||
|
||||
@@ -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,6 +5,11 @@
|
||||
[](https://optuna-dashboard.readthedocs.io/en/latest/?badge=latest)
|
||||
[](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
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
-------------------
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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."}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Generated
+14480
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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" }} />
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import { mergeUnionSearchSpace } from "../optuna_dashboard/ts/searchSpace"
|
||||
import { mergeUnionSearchSpace } from "../searchSpace"
|
||||
|
||||
global.URL.createObjectURL = jest.fn()
|
||||
|
||||
Vendored
+5
-6
@@ -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/",
|
||||
},
|
||||
Generated
+298
-25309
File diff suppressed because it is too large
Load Diff
+4
-63
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -4,4 +4,4 @@ ignore =
|
||||
W503
|
||||
max-line-length = 99
|
||||
statistics = True
|
||||
exclude = venv,build
|
||||
exclude = venv,build,node_modules
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup()
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Generated
+2304
-1329
File diff suppressed because it is too large
Load Diff
+19
-23
@@ -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 |
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { atom } from "recoil"
|
||||
|
||||
export const studiesState = atom<Study[]>({
|
||||
key: "studies",
|
||||
default: [],
|
||||
})
|
||||
Vendored
+16
-6
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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: ['..'],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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()
|
||||
]
|
||||
};
|
||||
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "tslib",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Generated
+43
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import * as Optuna from "@optuna/types"
|
||||
|
||||
export type OptunaStorage = {
|
||||
getStudies: () => Promise<Optuna.StudySummary[]>
|
||||
getStudy: (idx: number) => Promise<Optuna.Study | null>
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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"]
|
||||
}
|
||||
Generated
+13
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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!**
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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
Reference in New Issue
Block a user