mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-09 11:28:14 +08:00
Merge branch 'main' into test-orthants-mvn-gibbs-sampling
This commit is contained in:
+2
-1
@@ -5,7 +5,8 @@ module.exports = {
|
||||
'@typescript-eslint',
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/ban-ts-comment": "off"
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"eqeqeq": ["error", "smart"],
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
name: e2e-tests
|
||||
name: e2e-dashboard-tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.github/workflows/e2e-tests.yml'
|
||||
- '.github/workflows/e2e-dashboard-tests.yml'
|
||||
- '**.py'
|
||||
- '**.ts'
|
||||
- '**.tsx'
|
||||
@@ -47,4 +47,10 @@ jobs:
|
||||
run: playwright install
|
||||
|
||||
- name: Run e2e tests
|
||||
run: pytest e2e_tests
|
||||
run: |
|
||||
if [ "${{ matrix.optuna-version }}" = "optuna==2.10.0" ]; then
|
||||
ignore_option="--ignore e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py"
|
||||
else
|
||||
ignore_option=""
|
||||
fi
|
||||
pytest e2e_tests/test_dashboard $ignore_option
|
||||
@@ -0,0 +1,51 @@
|
||||
name: e2e-standalone-tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.github/workflows/e2e-standalone-tests.yml'
|
||||
- '**.py'
|
||||
- '**.ts'
|
||||
- '**.tsx'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'tsconfig.json'
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
optuna-version: ['optuna==2.10.0', 'git+https://github.com/optuna/optuna.git']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust toolchains
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Setup Optuna ${{ matrix.optuna-version }}
|
||||
run: |
|
||||
python -m pip install --progress-bar off --upgrade pip setuptools
|
||||
python -m pip install --progress-bar off --upgrade ${{ matrix.optuna-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --progress-bar off .
|
||||
python -m pip install --progress-bar off pytest-playwright
|
||||
|
||||
- name: Build standalone_app
|
||||
run: make MODE="prd" standalone_app/public/bundle.js
|
||||
|
||||
- name: Install the required browsers
|
||||
run: playwright install
|
||||
|
||||
- name: Run e2e tests
|
||||
run: pytest e2e_tests/test_standalone
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
paths:
|
||||
- '.github/workflows/python-tests.yml'
|
||||
- '**.py'
|
||||
- 'pyproject.toml'
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -57,7 +58,7 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
python-version: '3.11'
|
||||
architecture: x64
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
@@ -39,5 +39,4 @@ coverage.xml
|
||||
.vscode/
|
||||
.DS_Store
|
||||
tmp/
|
||||
examples/preferential-optimization/artifact/
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ $ pytest python_tests/
|
||||
|
||||
```
|
||||
$ pip install -r requirements.txt
|
||||
$ playwright install
|
||||
$ pytest e2e_tests
|
||||
```
|
||||
|
||||
@@ -92,6 +93,9 @@ If you want to create a screenshot for each test, please run a following command
|
||||
$ pytest e2e_tests --screenshot on --output tmp
|
||||
```
|
||||
|
||||
If you want to generate a locator in each webpage, please use the playwright codegen. See [this page](https://playwright.dev/python/docs/codegen-intro) for more details.
|
||||
|
||||
|
||||
For more detail options, you can check [this page](https://playwright.dev/python/docs/test-runners).
|
||||
|
||||
#### Linters (flake8, black and mypy)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||

|
||||
[](https://pypistats.org/packages/optuna-dashboard)
|
||||
[](https://optuna-dashboard.readthedocs.io/en/latest/?badge=latest)
|
||||
[](https://codecov.io/gh/optuna/optuna-dashboard)
|
||||
|
||||
|
||||
Real-time dashboard for [Optuna](https://github.com/optuna/optuna).
|
||||
@@ -77,6 +78,18 @@ $ docker run -it --rm -p 8080:8080 ghcr.io/optuna/optuna-dashboard postgresql+ps
|
||||
|
||||
</details>
|
||||
|
||||
## Jupyter Lab Extension (Experimental)
|
||||
|
||||
You can install the Jupyter Lab extension via [PyPI](https://pypi.org/project/jupyterlab-optuna/).
|
||||
|
||||
```
|
||||
$ pip install jupyterlab jupyterlab-optuna
|
||||
```
|
||||
|
||||
<img src="./docs/_static/jupyterlab-extension.png" style="width:600px;" alt="Jupyter Lab Extension">
|
||||
|
||||
To use, click the tile to launch the extension, and enter your Optuna’s storage URL (e.g. `sqlite:///db.sqlite3`) in the dialog.
|
||||
|
||||
## Browser-only version (Experimental)
|
||||
|
||||
<img src="./docs/_static/browser-app.gif" style="width:600px;" alt="Browser-only version">
|
||||
@@ -93,7 +106,7 @@ https://optuna.github.io/optuna-dashboard/
|
||||
|
||||
You can install the VS Code extension via [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=Optuna.optuna-dashboard#overview).
|
||||
|
||||
<img src="./docs/_static/vscode-extension.png" style="width:600px;" alt="VSCode Extension">
|
||||
<img src="./docs/_static/vscode-extension.png" style="width:600px;" alt="VS Code Extension">
|
||||
|
||||
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.
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 244 KiB |
@@ -44,6 +44,7 @@ Preferential Optimization
|
||||
optuna_dashboard.preferential.create_study
|
||||
optuna_dashboard.preferential.load_study
|
||||
optuna_dashboard.preferential.PreferentialStudy
|
||||
optuna_dashboard.preferential.samplers.gp.PreferentialGPSampler
|
||||
optuna_dashboard.register_preference_feedback_component
|
||||
|
||||
Streamlit
|
||||
|
||||
@@ -178,6 +178,19 @@ or
|
||||
$ pip install uwsgi
|
||||
$ uwsgi --http :8080 --workeers 4 --wsgi-file wsgi.py
|
||||
|
||||
Jupyter Lab Extension (Experimental)
|
||||
--------------------------------
|
||||
|
||||
You can install the Jupyter Lab extension via `PyPI <https://pypi.org/project/jupyterlab-optuna/>`_.
|
||||
|
||||
.. figure:: _static/jupyterlab-extension.png
|
||||
:alt: Screenshot for the Jupyter Lab Extension
|
||||
:align: center
|
||||
:width: 800px
|
||||
|
||||
Jupyter Lab Extension
|
||||
|
||||
To use, click the tile to launch the extension, and enter your Optuna’s storage URL (e.g. ``sqlite:///db.sqlite3``) in the dialog.
|
||||
|
||||
Browser-only version (Experimental)
|
||||
-----------------------------------
|
||||
|
||||
+14
-14
@@ -95,7 +95,7 @@ Given the above system, we carry out HITL optimization as follows:
|
||||
Environment setup
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
To run `the script <https://github.com/optuna/optuna-dashboard/blob/main/examples/hitl/main.py>`_ used in this tutorial, you need to install following libraries:
|
||||
To run `the script <https://github.com/optuna/optuna-examples/blob/main/dashboard/hitl/main.py>`_ used in this tutorial, you need to install following libraries:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
@@ -109,7 +109,7 @@ You will use SQLite for the storage backend in this tutorial. Ensure that the fo
|
||||
Execution of the HITL optimization script
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Run a python script below which you copied from `main.py <https://github.com/optuna/optuna-dashboard/blob/main/examples/hitl/main.py>`_
|
||||
Run a python script below which you copied from `main.py <https://github.com/optuna/optuna-examples/blob/main/dashboard/hitl/main.py>`_
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
@@ -150,7 +150,7 @@ Click the third item in the sidebar. You will see a list of all trials.
|
||||
|
||||
.. image:: ./images/hitl10.png
|
||||
|
||||
For each trial, you can see its details such as RGB parameter values and importantly, the generated image based on these values.
|
||||
For each trial, you can see its details such as RGB parameter values and importantly, the generated image based on these values.
|
||||
|
||||
.. image:: ./images/hitl11.gif
|
||||
:width: 90%
|
||||
@@ -189,21 +189,21 @@ Let’s walk through the script we used for the optimization.
|
||||
r = trial.suggest_int("r", 0, 255)
|
||||
g = trial.suggest_int("g", 0, 255)
|
||||
b = trial.suggest_int("b", 0, 255)
|
||||
|
||||
|
||||
# 2. Generate image
|
||||
image_path = f"tmp/sample-{trial.number}.png"
|
||||
image = Image.new("RGB", (320, 240), color=(r, g, b))
|
||||
image.save(image_path)
|
||||
|
||||
|
||||
# 3. Upload Artifact
|
||||
artifact_id = upload_artifact(trial, image_path, artifact_store)
|
||||
artifact_path = get_artifact_path(trial, artifact_id)
|
||||
|
||||
|
||||
# 4. Save Note
|
||||
note = textwrap.dedent(
|
||||
f"""\
|
||||
## Trial {trial.number}
|
||||
|
||||
|
||||

|
||||
"""
|
||||
)
|
||||
@@ -222,10 +222,10 @@ In the ``suggest_and_generate_image`` function, a new Trial is obtained and new
|
||||
sampler=optuna.samplers.TPESampler(constant_liar=True, n_startup_trials=5),
|
||||
load_if_exists=True,
|
||||
)
|
||||
|
||||
|
||||
# 2. Set an objective name
|
||||
study.set_metric_names(["Looks like sunset color?"])
|
||||
|
||||
|
||||
# 3. Register ChoiceWidget
|
||||
register_objective_form_widgets(
|
||||
study,
|
||||
@@ -237,7 +237,7 @@ In the ``suggest_and_generate_image`` function, a new Trial is obtained and new
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# 4. Start Human-in-the-loop Optimization
|
||||
n_batch = 4
|
||||
while True:
|
||||
@@ -259,17 +259,17 @@ The function ``start_optimization`` defines our loop for HITL optimization to ge
|
||||
|
||||
def main() -> NoReturn:
|
||||
tmp_path = os.path.join(os.path.dirname(__file__), "tmp")
|
||||
|
||||
|
||||
# 1. Create Artifact Store
|
||||
artifact_path = os.path.join(os.path.dirname(__file__), "artifact")
|
||||
artifact_store = FileSystemArtifactStore(artifact_path)
|
||||
|
||||
|
||||
if not os.path.exists(artifact_path):
|
||||
os.mkdir(artifact_path)
|
||||
|
||||
|
||||
if not os.path.exists(tmp_path):
|
||||
os.mkdir(tmp_path)
|
||||
|
||||
|
||||
# 2. Run optimize loop
|
||||
start_optimization(artifact_store)
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ What is Preferential Optimization?
|
||||
----------------------------------
|
||||
|
||||
Preferential optimization is a method for optimizing hyperparameters, focusing of human preferences, by determining which trial is superior when comparing a pair.
|
||||
It differs from `human-in-the-loop optimization utilizing objective form widgets <tutorial-hitl-objective-form-widgets>`_,
|
||||
It differs from :ref:`human-in-the-loop optimization utilizing objective form widgets <tutorial-hitl-objective-form-widgets>`,
|
||||
which relies on absolute evaluations, as it significantly reduces fluctuations in evaluators' criteria, thus ensuring more consistent results.
|
||||
|
||||
In this tutorial, we'll interactively optimize RGB values to generate a color resembling a "sunset hue",
|
||||
aligining with the problem setting in `this tutorial <tutorial-hitl-objective-form-widgets>`_.
|
||||
aligining with the problem setting in :ref:`this tutorial <tutorial-hitl-objective-form-widgets>`.
|
||||
Familiarity with the tutorial ob objective form widgets may enhance your understanding.
|
||||
|
||||
How to Run Preferential Optimization
|
||||
@@ -27,7 +27,7 @@ First, ensure the necessary packages are installed by executing the following co
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install "optuna>=3.3.0" "optuna-dashboard>=0.13.0b1" pillow botorch
|
||||
$ pip install "optuna>=3.3.0" "optuna-dashboard[preferential]>=0.13.0b1" pillow
|
||||
|
||||
Next, execute the Python script, copied from `generator.py`_.
|
||||
|
||||
@@ -99,7 +99,7 @@ enabling the Optuna Dashboard to display images on the evaluation feedback page.
|
||||
register_preference_feedback_component(study, "artifact", user_attr_key)
|
||||
|
||||
Following this, we create a loop that continuously checks if new trials should be generated, awaiting human evaluation if not.
|
||||
Within the while loop, new trials are generated if the condition :meth:`~optuna_dashboard.preferential.PreferentialStudy.should_generate` returns ``True``.
|
||||
Within the while loop, new trials are generated if the condition :meth:`~optuna_dashboard.preferential.PreferentialStudy.should_generate` returns ``True``.
|
||||
For each trial, RGB values are sampled, an image is generated with these values, saved temporarily.
|
||||
Then the image is uploaded to the artifact store, and finally, the ``artifact_id`` is stored to the key, which is specified via :func:`~optuna_dashboard.register_preference_feedback_component`.
|
||||
|
||||
@@ -127,4 +127,4 @@ Then the image is uploaded to the artifact store, and finally, the ``artifact_id
|
||||
artifact_id = upload_artifact(trial, image_path, artifact_store)
|
||||
trial.set_user_attr(user_attr_key, artifact_id)
|
||||
|
||||
.. _generator.py: https://github.com/optuna/optuna-dashboard/blob/main/examples/preferential-optimization/generator.py
|
||||
.. _generator.py: https://github.com/optuna/optuna-examples/blob/main/dashboard/preferential-optimization/generator.py
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import re
|
||||
|
||||
import optuna
|
||||
from optuna.trial import TrialState
|
||||
from optuna_dashboard import ChoiceWidget
|
||||
from optuna_dashboard import register_objective_form_widgets
|
||||
from playwright.sync_api import expect
|
||||
from playwright.sync_api import Page
|
||||
import pytest
|
||||
|
||||
from ...test_server import make_test_server
|
||||
from ...utils import clear_inmemory_cache
|
||||
|
||||
|
||||
def make_test_storage() -> optuna.storages.InMemoryStorage:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
sampler = optuna.samplers.RandomSampler(seed=0)
|
||||
|
||||
study = optuna.create_study(
|
||||
study_name="preferential_optimization",
|
||||
storage=storage,
|
||||
sampler=sampler,
|
||||
)
|
||||
|
||||
register_objective_form_widgets(
|
||||
study,
|
||||
widgets=[
|
||||
ChoiceWidget(
|
||||
choices=["Good", "So-so", "Bad"],
|
||||
values=[-1, 0, 1],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
n_batch = 4
|
||||
while True:
|
||||
running_trials = study.get_trials(deepcopy=False, states=(TrialState.RUNNING,))
|
||||
if len(running_trials) >= n_batch:
|
||||
break
|
||||
study.ask()
|
||||
|
||||
return storage
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def storage() -> optuna.storages.InMemoryStorage:
|
||||
clear_inmemory_cache()
|
||||
storage = make_test_storage()
|
||||
return storage
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def server_url(request: pytest.FixtureRequest, storage: optuna.storages.InMemoryStorage) -> str:
|
||||
return make_test_server(request, storage)
|
||||
|
||||
|
||||
def test_preferential_optimization(
|
||||
page: Page,
|
||||
storage: optuna.storages.InMemoryStorage,
|
||||
server_url: str,
|
||||
) -> None:
|
||||
summaries = optuna.get_all_study_summaries(storage)
|
||||
study_id = summaries[0]._study_id
|
||||
url = f"{server_url}/studies/{study_id}/trials"
|
||||
|
||||
page.goto(url)
|
||||
|
||||
# Confirm that the trial list page is displayed.
|
||||
expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text(
|
||||
"Trial 0 (trial_id=0)"
|
||||
)
|
||||
page.get_by_label("Filter").click()
|
||||
# Confirm that all trials are running.
|
||||
expect(
|
||||
page.get_by_text("Complete (0)Pruned (0)Fail (0)Running (4)Waiting (0)")
|
||||
).to_be_visible()
|
||||
page.locator(".MuiBackdrop-root").click()
|
||||
|
||||
# Confirm that the trial detail page is displayed.
|
||||
expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text(
|
||||
"Trial 0 (trial_id=0)"
|
||||
)
|
||||
# This trial is running.
|
||||
expect(page.get_by_text("Running", exact=True).nth(4)).to_be_visible()
|
||||
page.get_by_label("Bad").check()
|
||||
page.get_by_role("button", name="Submit").click()
|
||||
# This trial is completed and is the best trial.
|
||||
expect(page.get_by_text("Complete").nth(1)).to_be_visible()
|
||||
expect(page.get_by_text("Best Trial").nth(1)).to_be_visible()
|
||||
|
||||
# Move the next trial page.
|
||||
page.get_by_role("button", name="Trial 1 Running").click()
|
||||
# Confirm that the trial detail page is displayed.
|
||||
expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text(
|
||||
"Trial 1 (trial_id=1)"
|
||||
)
|
||||
# This trial is running.
|
||||
expect(page.get_by_text("Running", exact=True).nth(3)).to_be_visible()
|
||||
page.get_by_label("So-so").check()
|
||||
page.get_by_role("button", name="Submit").click()
|
||||
# This trial is completed and is the best trial.
|
||||
expect(page.get_by_text("Complete").nth(2)).to_be_visible()
|
||||
expect(page.get_by_text("Best Trial").nth(1)).to_be_visible()
|
||||
|
||||
# Move the next trial page.
|
||||
page.get_by_role("button", name="Trial 2 Running").click()
|
||||
# Confirm that the trial detail page is displayed.
|
||||
expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text(
|
||||
"Trial 2 (trial_id=2)"
|
||||
)
|
||||
# This trial is running.
|
||||
expect(page.get_by_text("Running", exact=True).nth(2)).to_be_visible()
|
||||
page.get_by_label("Good").check()
|
||||
page.get_by_role("button", name="Submit").click()
|
||||
# This trial is completed and is the best trial.
|
||||
expect(page.get_by_text("Complete").nth(3)).to_be_visible()
|
||||
expect(page.get_by_text("Best Trial").nth(1)).to_be_visible()
|
||||
|
||||
# Move the next trial page.
|
||||
page.get_by_role("button", name="Trial 3 Running").click()
|
||||
# Confirm that the trial detail page is displayed.
|
||||
expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text(
|
||||
"Trial 3 (trial_id=3)"
|
||||
)
|
||||
# This trial is running.
|
||||
expect(page.get_by_text("Running", exact=True).nth(1)).to_be_visible()
|
||||
page.get_by_role("button", name="Fail Trial").click()
|
||||
# This trial is failed.
|
||||
expect(page.get_by_text("Fail").nth(1)).to_be_visible()
|
||||
+3
-1
@@ -2,7 +2,8 @@ import optuna
|
||||
from playwright.sync_api import Page
|
||||
import pytest
|
||||
|
||||
from ..test_server import make_test_server
|
||||
from ...test_server import make_test_server
|
||||
from ...utils import clear_inmemory_cache
|
||||
|
||||
|
||||
def make_test_storage() -> optuna.storages.InMemoryStorage:
|
||||
@@ -23,6 +24,7 @@ def make_test_storage() -> optuna.storages.InMemoryStorage:
|
||||
|
||||
@pytest.fixture
|
||||
def storage() -> optuna.storages.InMemoryStorage:
|
||||
clear_inmemory_cache()
|
||||
storage = make_test_storage()
|
||||
return storage
|
||||
|
||||
+3
-1
@@ -4,11 +4,13 @@ import optuna
|
||||
from playwright.sync_api import Page
|
||||
import pytest
|
||||
|
||||
from .test_server import make_test_server
|
||||
from ..test_server import make_test_server
|
||||
from ..utils import clear_inmemory_cache
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def storage() -> optuna.storages.InMemoryStorage:
|
||||
clear_inmemory_cache()
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
return storage
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import http.server
|
||||
import socket
|
||||
import socketserver
|
||||
import threading
|
||||
from wsgiref.simple_server import make_server
|
||||
|
||||
@@ -33,3 +35,29 @@ def make_test_server(
|
||||
request.addfinalizer(stop_server)
|
||||
|
||||
return f"http://{addr}:{port}/dashboard"
|
||||
|
||||
|
||||
def make_standalone_server(request: pytest.FixtureRequest) -> str:
|
||||
addr = "127.0.0.1"
|
||||
port = get_free_port()
|
||||
directory = "./standalone_app/"
|
||||
|
||||
Handler = http.server.SimpleHTTPRequestHandler
|
||||
httpd = socketserver.TCPServer(
|
||||
("", port), lambda *args, **kwargs: Handler(*args, directory=directory, **kwargs)
|
||||
)
|
||||
|
||||
def serve_httpd():
|
||||
httpd.serve_forever()
|
||||
|
||||
thread = threading.Thread(target=serve_httpd)
|
||||
thread.start()
|
||||
|
||||
def stop_server() -> None:
|
||||
httpd.shutdown()
|
||||
httpd.server_close()
|
||||
thread.join()
|
||||
|
||||
request.addfinalizer(stop_server)
|
||||
|
||||
return f"http://{addr}:{port}"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from playwright.sync_api import Page
|
||||
import pytest
|
||||
|
||||
from ..test_server import make_standalone_server
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def server_url(request: pytest.FixtureRequest) -> str:
|
||||
return make_standalone_server(request)
|
||||
|
||||
|
||||
def test_home(
|
||||
page: Page,
|
||||
server_url: str,
|
||||
) -> None:
|
||||
url = f"{server_url}"
|
||||
page.goto(url)
|
||||
element = page.get_by_role("heading")
|
||||
assert element is not None
|
||||
title = element.text_content()
|
||||
assert title is not None
|
||||
assert title == "Optuna Dashboard (Wasm ver.)"
|
||||
@@ -0,0 +1,9 @@
|
||||
from optuna_dashboard._storage import trials_cache
|
||||
from optuna_dashboard._storage import trials_cache_lock
|
||||
from optuna_dashboard._storage import trials_last_fetched_at
|
||||
|
||||
|
||||
def clear_inmemory_cache() -> None:
|
||||
with trials_cache_lock:
|
||||
trials_cache.clear()
|
||||
trials_last_fetched_at.clear()
|
||||
@@ -0,0 +1,5 @@
|
||||
Optuna Dashboard Examples
|
||||
=========================
|
||||
|
||||
Example files have been moved to the [optuna/optuna-examples](https://github.com/optuna/optuna-examples/) repoistory.
|
||||
You can find the dashboard-related examples in the [dashboard](https://github.com/optuna/optuna-examples/tree/main/dashboard) directory.
|
||||
@@ -1,98 +0,0 @@
|
||||
import os
|
||||
import textwrap
|
||||
import time
|
||||
from typing import NoReturn
|
||||
|
||||
import optuna
|
||||
from optuna.artifacts import FileSystemArtifactStore
|
||||
from optuna.artifacts import upload_artifact
|
||||
from optuna.trial import TrialState
|
||||
from optuna_dashboard import ChoiceWidget
|
||||
from optuna_dashboard import register_objective_form_widgets
|
||||
from optuna_dashboard import save_note
|
||||
from optuna_dashboard.artifact import get_artifact_path
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def suggest_and_generate_image(
|
||||
study: optuna.Study, artifact_store: FileSystemArtifactStore
|
||||
) -> None:
|
||||
# 1. Ask new parameters
|
||||
trial = study.ask()
|
||||
r = trial.suggest_int("r", 0, 255)
|
||||
g = trial.suggest_int("g", 0, 255)
|
||||
b = trial.suggest_int("b", 0, 255)
|
||||
|
||||
# 2. Generate image
|
||||
image_path = f"tmp/sample-{trial.number}.png"
|
||||
image = Image.new("RGB", (320, 240), color=(r, g, b))
|
||||
image.save(image_path)
|
||||
|
||||
# 3. Upload Artifact
|
||||
artifact_id = upload_artifact(trial, image_path, artifact_store)
|
||||
artifact_path = get_artifact_path(trial, artifact_id)
|
||||
|
||||
# 4. Save Note
|
||||
note = textwrap.dedent(
|
||||
f"""\
|
||||
## Trial {trial.number}
|
||||
|
||||

|
||||
"""
|
||||
)
|
||||
save_note(trial, note)
|
||||
|
||||
|
||||
def start_optimization(artifact_store: FileSystemArtifactStore) -> NoReturn:
|
||||
# 1. Create Study
|
||||
study = optuna.create_study(
|
||||
study_name="Human-in-the-loop Optimization",
|
||||
storage="sqlite:///db.sqlite3",
|
||||
sampler=optuna.samplers.TPESampler(constant_liar=True, n_startup_trials=5),
|
||||
load_if_exists=True,
|
||||
)
|
||||
|
||||
# 2. Set an objective name
|
||||
study.set_metric_names(["Looks like sunset color?"])
|
||||
|
||||
# 3. Register ChoiceWidget
|
||||
register_objective_form_widgets(
|
||||
study,
|
||||
widgets=[
|
||||
ChoiceWidget(
|
||||
choices=["Good 👍", "So-so👌", "Bad 👎"],
|
||||
values=[-1, 0, 1],
|
||||
description="Please input your score!",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# 4. Start Human-in-the-loop Optimization
|
||||
n_batch = 4
|
||||
while True:
|
||||
running_trials = study.get_trials(deepcopy=False, states=(TrialState.RUNNING,))
|
||||
if len(running_trials) >= n_batch:
|
||||
time.sleep(1) # Avoid busy-loop
|
||||
continue
|
||||
suggest_and_generate_image(study, artifact_store)
|
||||
|
||||
|
||||
def main() -> NoReturn:
|
||||
tmp_path = os.path.join(os.path.dirname(__file__), "tmp")
|
||||
|
||||
# 1. Create Artifact Store
|
||||
artifact_path = os.path.join(os.path.dirname(__file__), "artifact")
|
||||
artifact_store = FileSystemArtifactStore(artifact_path)
|
||||
|
||||
if not os.path.exists(artifact_path):
|
||||
os.mkdir(artifact_path)
|
||||
|
||||
if not os.path.exists(tmp_path):
|
||||
os.mkdir(tmp_path)
|
||||
|
||||
# 2. Run optimize loop
|
||||
start_optimization(artifact_store)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
optuna-dashboard sqlite:///example.db --artifact-dir ./artifact
|
||||
@@ -1,60 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
from typing import NoReturn
|
||||
|
||||
from optuna.artifacts import FileSystemArtifactStore
|
||||
from optuna.artifacts import upload_artifact
|
||||
from optuna_dashboard import register_preference_feedback_component
|
||||
from optuna_dashboard.preferential import create_study
|
||||
from optuna_dashboard.preferential.samplers.gp import PreferentialGPSampler
|
||||
from PIL import Image
|
||||
|
||||
|
||||
STORAGE_URL = "sqlite:///example.db"
|
||||
artifact_path = os.path.join(os.path.dirname(__file__), "artifact")
|
||||
artifact_store = FileSystemArtifactStore(base_path=artifact_path)
|
||||
os.makedirs(artifact_path, exist_ok=True)
|
||||
|
||||
|
||||
def main() -> NoReturn:
|
||||
study = create_study(
|
||||
n_generate=4,
|
||||
study_name="Preferential Optimization",
|
||||
storage=STORAGE_URL,
|
||||
sampler=PreferentialGPSampler(),
|
||||
load_if_exists=True,
|
||||
)
|
||||
# Change the component, displayed on the human feedback pages.
|
||||
# By default (component_type="note"), the Trial's Markdown note is displayed.
|
||||
user_attr_key = "rgb_image"
|
||||
register_preference_feedback_component(study, "artifact", user_attr_key)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
while True:
|
||||
# If study.should_generate() returns False,
|
||||
# the generator waits for human evaluation.
|
||||
if not study.should_generate():
|
||||
time.sleep(0.1) # Avoid busy-loop
|
||||
continue
|
||||
|
||||
trial = study.ask()
|
||||
# 1. Ask new parameters
|
||||
r = trial.suggest_int("r", 0, 255)
|
||||
g = trial.suggest_int("g", 0, 255)
|
||||
b = trial.suggest_int("b", 0, 255)
|
||||
|
||||
# 2. Generate image
|
||||
image_path = os.path.join(tmpdir, f"sample-{trial.number}.png")
|
||||
image = Image.new("RGB", (320, 240), color=(r, g, b))
|
||||
image.save(image_path)
|
||||
|
||||
# 3. Upload Artifact and set artifact_id to trial.user_attrs["rgb_image"].
|
||||
artifact_id = upload_artifact(trial, image_path, artifact_store)
|
||||
trial.set_user_attr(user_attr_key, artifact_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,54 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
import optuna
|
||||
from optuna.trial import TrialState
|
||||
from optuna_dashboard.artifact.file_system import FileSystemBackend
|
||||
from optuna_dashboard.streamlit import render_objective_form_widgets
|
||||
from optuna_dashboard.streamlit import render_trial_note
|
||||
|
||||
import streamlit as st
|
||||
|
||||
|
||||
artifact_path = os.path.join(os.path.dirname(__file__), "artifact")
|
||||
artifact_backend = FileSystemBackend(base_path=artifact_path)
|
||||
|
||||
|
||||
def get_tmp_dir() -> str:
|
||||
if "tmp_dir" not in st.session_state:
|
||||
tmp_dir_name = str(uuid.uuid4())
|
||||
tmp_dir_path = os.path.join(tempfile.gettempdir(), tmp_dir_name)
|
||||
os.makedirs(tmp_dir_path, exist_ok=True)
|
||||
st.session_state.tmp_dir = tmp_dir_path
|
||||
|
||||
return st.session_state.tmp_dir
|
||||
|
||||
|
||||
def start_streamlit() -> None:
|
||||
tmpdir = get_tmp_dir()
|
||||
study = optuna.load_study(
|
||||
storage="sqlite:///streamlit-db.sqlite3", study_name="Human-in-the-loop Optimization"
|
||||
)
|
||||
selected_trial = st.sidebar.selectbox("Trial", study.trials, format_func=lambda t: t.number)
|
||||
|
||||
if selected_trial is None:
|
||||
return
|
||||
render_trial_note(study, selected_trial)
|
||||
artifact_id = selected_trial.user_attrs.get("artifact_id")
|
||||
if artifact_id:
|
||||
with artifact_backend.open(artifact_id) as fsrc:
|
||||
tmp_img_path = os.path.join(tmpdir, artifact_id + ".png")
|
||||
with open(tmp_img_path, "wb") as fdst:
|
||||
shutil.copyfileobj(fsrc, fdst)
|
||||
st.image(tmp_img_path, caption="Image")
|
||||
|
||||
if selected_trial.state == TrialState.RUNNING:
|
||||
render_objective_form_widgets(study, selected_trial)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
start_streamlit()
|
||||
@@ -1,81 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
from typing import NoReturn
|
||||
|
||||
import optuna
|
||||
from optuna.trial import TrialState
|
||||
from optuna_dashboard import ChoiceWidget
|
||||
from optuna_dashboard import register_objective_form_widgets
|
||||
from optuna_dashboard import save_note
|
||||
from optuna_dashboard.artifact import upload_artifact
|
||||
from optuna_dashboard.artifact.file_system import FileSystemBackend
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def suggest_and_generate_image(
|
||||
study: optuna.Study, artifact_backend: FileSystemBackend, tmpdir: str
|
||||
) -> None:
|
||||
# 1. Ask new parameters
|
||||
trial = study.ask()
|
||||
r = trial.suggest_int("r", 0, 255)
|
||||
g = trial.suggest_int("g", 0, 255)
|
||||
b = trial.suggest_int("b", 0, 255)
|
||||
|
||||
# 2. Generate image
|
||||
image_path = os.path.join(tmpdir, f"sample-{trial.number}.png")
|
||||
image = Image.new("RGB", (320, 240), color=(r, g, b))
|
||||
image.save(image_path)
|
||||
|
||||
# 3. Upload Artifact
|
||||
artifact_id = upload_artifact(artifact_backend, trial, image_path)
|
||||
trial.set_user_attr("artifact_id", artifact_id)
|
||||
|
||||
# 4. Save Note
|
||||
save_note(trial, f"## Trial {trial.number}")
|
||||
|
||||
|
||||
def main() -> NoReturn:
|
||||
# 1. Create Artifact Store
|
||||
artifact_path = os.path.join(os.path.dirname(__file__), "artifact")
|
||||
artifact_backend = FileSystemBackend(base_path=artifact_path)
|
||||
|
||||
if not os.path.exists(artifact_path):
|
||||
os.mkdir(artifact_path)
|
||||
|
||||
# 2. Create Study
|
||||
study = optuna.create_study(
|
||||
study_name="Human-in-the-loop Optimization",
|
||||
storage="sqlite:///streamlit-db.sqlite3",
|
||||
sampler=optuna.samplers.TPESampler(constant_liar=True, n_startup_trials=5),
|
||||
load_if_exists=True,
|
||||
)
|
||||
study.set_metric_names(["Looks like sunset color?"])
|
||||
|
||||
# 4. Register ChoiceWidget
|
||||
register_objective_form_widgets(
|
||||
study,
|
||||
widgets=[
|
||||
ChoiceWidget(
|
||||
choices=["Good 👍", "So-so👌", "Bad 👎"],
|
||||
values=[-1, 0, 1],
|
||||
description="Please input your score!",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# 5. Start Human-in-the-loop Optimization
|
||||
n_batch = 4
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
while True:
|
||||
running_trials = study.get_trials(deepcopy=False, states=(TrialState.RUNNING,))
|
||||
if len(running_trials) >= n_batch:
|
||||
time.sleep(1) # Avoid busy-loop
|
||||
continue
|
||||
suggest_and_generate_image(study, artifact_backend, tmpdir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -17,4 +17,4 @@ from ._note import save_note # noqa
|
||||
from ._preference_setting import register_preference_feedback_component # noqa
|
||||
|
||||
|
||||
__version__ = "0.13.0b1"
|
||||
__version__ = "0.14.0"
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import functools
|
||||
import io
|
||||
from itertools import chain
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import typing
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
@@ -152,6 +156,7 @@ def create_app(
|
||||
storage=storage, study_name=dst_study_name, directions=src_study.directions
|
||||
)
|
||||
dst_study.add_trials(src_study.get_trials(deepcopy=False))
|
||||
note.copy_notes(storage, src_study, dst_study)
|
||||
except DuplicatedStudyError:
|
||||
response.status = 400 # Bad request
|
||||
return {"reason": f"study_name={dst_study_name} is duplicaated"}
|
||||
@@ -448,6 +453,48 @@ def create_app(
|
||||
response.status = 204 # No content
|
||||
return {}
|
||||
|
||||
@app.get("/csv/<study_id:int>")
|
||||
def download_csv(study_id: int) -> BottleViewReturn:
|
||||
# Create a CSV file
|
||||
try:
|
||||
study_name = storage.get_study_name_from_id(study_id)
|
||||
study = optuna.load_study(storage=storage, study_name=study_name)
|
||||
except KeyError:
|
||||
response.status = 404 # Not found
|
||||
return {"reason": f"study_id={study_id} is not found"}
|
||||
trials = study.trials
|
||||
param_names = sorted(set(chain.from_iterable([t.params.keys() for t in trials])))
|
||||
user_attr_names = sorted(set(chain.from_iterable([t.user_attrs.keys() for t in trials])))
|
||||
param_names_header = [f"Param {x}" for x in param_names]
|
||||
user_attr_names_header = [f"UserAttribute {x}" for x in user_attr_names]
|
||||
n_objs = len(study.directions)
|
||||
if study.metric_names is not None:
|
||||
value_header = study.metric_names
|
||||
else:
|
||||
value_header = ["Value"] if n_objs == 1 else [f"Objective {x}" for x in range(n_objs)]
|
||||
column_names = (
|
||||
["Number", "State"] + value_header + param_names_header + user_attr_names_header
|
||||
)
|
||||
|
||||
buf = io.StringIO("")
|
||||
writer = csv.writer(buf)
|
||||
writer.writerow(column_names)
|
||||
for frozen_trial in trials:
|
||||
row = [frozen_trial.number, frozen_trial.state.name]
|
||||
row.extend(frozen_trial.values if frozen_trial.values is not None else [None] * n_objs)
|
||||
row.extend([frozen_trial.params.get(name, None) for name in param_names])
|
||||
row.extend([frozen_trial.user_attrs.get(name, None) for name in user_attr_names])
|
||||
writer.writerow(row)
|
||||
|
||||
# Set response headers
|
||||
output_name = "-".join(re.sub(r'[\\/:*?"<>|]+', "", study_name).split(" "))
|
||||
response.headers["Content-Type"] = "text/csv; chatset=cp932"
|
||||
response.headers["Content-Disposition"] = f"attachment; filename={output_name}.csv"
|
||||
|
||||
# Response body
|
||||
buf.seek(0)
|
||||
return buf.read()
|
||||
|
||||
@app.get("/favicon.ico")
|
||||
def favicon() -> BottleViewReturn:
|
||||
use_gzip = "gzip" in request.headers["Accept-Encoding"]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import numbers
|
||||
import threading
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
@@ -85,9 +86,8 @@ class _CachedExtraStudyProperty:
|
||||
self._cursor = next_cursor
|
||||
|
||||
def _update_user_attrs(self, trial: FrozenTrial) -> None:
|
||||
# TODO(c-bata): Support numpy-specific number types.
|
||||
current_user_attrs = {
|
||||
k: not isinstance(v, bool) and isinstance(v, (int, float))
|
||||
k: not isinstance(v, bool) and isinstance(v, numbers.Real)
|
||||
for k, v in trial.user_attrs.items()
|
||||
}
|
||||
for attr_name, current_is_sortable in current_user_attrs.items():
|
||||
|
||||
@@ -110,6 +110,19 @@ def note_str_key_prefix(trial_id: Optional[int]) -> str:
|
||||
return f"dashboard:{trial_id}:note_str:"
|
||||
|
||||
|
||||
def copy_notes(storage: BaseStorage, src_study: optuna.Study, dst_study: optuna.Study) -> None:
|
||||
system_attrs = storage.get_study_system_attrs(study_id=src_study._study_id)
|
||||
|
||||
# Copy individual trial notes
|
||||
for src_trial, dst_trial in zip(src_study.get_trials(), dst_study.get_trials()):
|
||||
note = get_note_from_system_attrs(system_attrs, src_trial._trial_id)["body"]
|
||||
save_note_with_version(storage, dst_study._study_id, dst_trial._trial_id, 0, note)
|
||||
|
||||
# Copy study note
|
||||
note = get_note_from_system_attrs(system_attrs, None)["body"]
|
||||
save_note_with_version(storage, dst_study._study_id, None, 0, note)
|
||||
|
||||
|
||||
def get_note_from_system_attrs(system_attrs: dict[str, Any], trial_id: Optional[int]) -> NoteType:
|
||||
if note_ver_key(trial_id) not in system_attrs:
|
||||
return {
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
import numbers
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
@@ -104,6 +105,8 @@ def serialize_attrs(attrs: dict[str, Any]) -> list[Attribute]:
|
||||
value = "<binary object>"
|
||||
elif isinstance(v, str):
|
||||
value = v
|
||||
elif isinstance(v, numbers.Real):
|
||||
value = str(v)
|
||||
else:
|
||||
value = json.dumps(v)
|
||||
value = value[:MAX_ATTR_LENGTH] if len(value) > MAX_ATTR_LENGTH else value
|
||||
|
||||
@@ -105,7 +105,7 @@ def register_artifact_route(
|
||||
|
||||
@app.post("/api/artifacts/<study_id:int>/<trial_id:int>")
|
||||
@json_api_view
|
||||
def upload_artifact_api(study_id: int, trial_id: int) -> dict[str, Any]:
|
||||
def upload_trial_artifact_api(study_id: int, trial_id: int) -> dict[str, Any]:
|
||||
trial = storage.get_trial(trial_id)
|
||||
if trial is None:
|
||||
response.status = 400
|
||||
@@ -139,14 +139,48 @@ def register_artifact_route(
|
||||
storage.set_trial_system_attr(trial_id, attr_key, json.dumps(artifact))
|
||||
response.status = 201
|
||||
|
||||
trial = storage.get_trial(trial_id) # Fetch trial.system_attrs again.
|
||||
return {
|
||||
"artifact_id": artifact_id,
|
||||
"artifacts": list_trial_artifacts(storage.get_study_system_attrs(study_id), trial),
|
||||
}
|
||||
|
||||
@app.post("/api/artifacts/<study_id:int>")
|
||||
@json_api_view
|
||||
def upload_study_artifact_api(study_id: int) -> dict[str, Any]:
|
||||
if artifact_store is None:
|
||||
response.status = 400 # Bad Request
|
||||
return {"reason": "Cannot access to the artifacts."}
|
||||
file = request.json.get("file")
|
||||
if file is None:
|
||||
response.status = 400
|
||||
return {"reason": "Please specify the 'file' key."}
|
||||
|
||||
_, data = parse_data_uri(file)
|
||||
filename = request.json.get("filename", "")
|
||||
artifact_id = str(uuid.uuid4())
|
||||
artifact_store.write(artifact_id, io.BytesIO(data))
|
||||
|
||||
mimetype, encoding = mimetypes.guess_type(filename)
|
||||
artifact = {
|
||||
"artifact_id": artifact_id,
|
||||
"filename": filename,
|
||||
"mimetype": mimetype or DEFAULT_MIME_TYPE,
|
||||
"encoding": encoding,
|
||||
}
|
||||
attr_key = ARTIFACTS_ATTR_PREFIX + artifact_id
|
||||
storage.set_study_system_attr(study_id, attr_key, json.dumps(artifact))
|
||||
|
||||
response.status = 201
|
||||
|
||||
return {
|
||||
"artifact_id": artifact_id,
|
||||
"artifacts": list_study_artifacts(storage.get_study_system_attrs(study_id)),
|
||||
}
|
||||
|
||||
@app.delete("/api/artifacts/<study_id:int>/<trial_id:int>/<artifact_id:re:[0-9a-fA-F-]+>")
|
||||
@json_api_view
|
||||
def delete_artifact(study_id: int, trial_id: int, artifact_id: str) -> dict[str, Any]:
|
||||
def delete_trial_artifact(study_id: int, trial_id: int, artifact_id: str) -> dict[str, Any]:
|
||||
if artifact_store is None:
|
||||
response.status = 400 # Bad Request
|
||||
return {"reason": "Cannot access to the artifacts."}
|
||||
@@ -154,7 +188,7 @@ def register_artifact_route(
|
||||
|
||||
# The artifact's metadata is stored in one of the following two locations:
|
||||
storage.set_study_system_attr(
|
||||
study_id, _dashboard_trial_artifact_prefix(trial_id) + artifact_id, json.dumps(None)
|
||||
study_id, _dashboard_artifact_prefix(trial_id) + artifact_id, json.dumps(None)
|
||||
)
|
||||
storage.set_trial_system_attr(
|
||||
trial_id, ARTIFACTS_ATTR_PREFIX + artifact_id, json.dumps(None)
|
||||
@@ -163,6 +197,21 @@ def register_artifact_route(
|
||||
response.status = 204
|
||||
return {}
|
||||
|
||||
@app.delete("/api/artifacts/<study_id:int>/<artifact_id:re:[0-9a-fA-F-]+>")
|
||||
@json_api_view
|
||||
def delete_study_artifact(study_id: int, artifact_id: str) -> dict[str, Any]:
|
||||
if artifact_store is None:
|
||||
response.status = 400 # Bad Request
|
||||
return {"reason": "Cannot access to the artifacts."}
|
||||
artifact_store.remove(artifact_id)
|
||||
|
||||
storage.set_study_system_attr(
|
||||
study_id, ARTIFACTS_ATTR_PREFIX + artifact_id, json.dumps(None)
|
||||
)
|
||||
|
||||
response.status = 204
|
||||
return {}
|
||||
|
||||
|
||||
def upload_artifact(
|
||||
backend: ArtifactBackend,
|
||||
@@ -220,7 +269,7 @@ def upload_artifact(
|
||||
return artifact_id
|
||||
|
||||
|
||||
def _dashboard_trial_artifact_prefix(trial_id: int) -> str:
|
||||
def _dashboard_artifact_prefix(trial_id: int) -> str:
|
||||
return DASHBOARD_ARTIFACTS_ATTR_PREFIX + f"{trial_id}:"
|
||||
|
||||
|
||||
@@ -240,7 +289,7 @@ def get_trial_artifact_meta(
|
||||
) -> Optional[ArtifactMeta]:
|
||||
# Search study_system_attrs due to backward compatibility.
|
||||
study_system_attrs = storage.get_study_system_attrs(study_id)
|
||||
attr_key = _dashboard_trial_artifact_prefix(trial_id=trial_id) + artifact_id
|
||||
attr_key = _dashboard_artifact_prefix(trial_id=trial_id) + artifact_id
|
||||
artifact_meta = study_system_attrs.get(attr_key)
|
||||
if artifact_meta is not None:
|
||||
return json.loads(artifact_meta)
|
||||
@@ -284,7 +333,7 @@ def list_trial_artifacts(
|
||||
dashboard_artifact_metas = [
|
||||
json.loads(value)
|
||||
for key, value in study_system_attrs.items()
|
||||
if key.startswith(_dashboard_trial_artifact_prefix(trial._trial_id))
|
||||
if key.startswith(_dashboard_artifact_prefix(trial._trial_id))
|
||||
]
|
||||
|
||||
# Collect ArtifactMeta from trial_system_attrs. Note that artifacts uploaded via
|
||||
|
||||
@@ -7,9 +7,9 @@ from typing import Iterable
|
||||
|
||||
import optuna
|
||||
from optuna import logging
|
||||
from optuna._imports import try_import
|
||||
from optuna.distributions import BaseDistribution
|
||||
from optuna.samplers import BaseSampler
|
||||
from optuna.samplers import RandomSampler
|
||||
from optuna.trial import FrozenTrial
|
||||
from optuna.trial import TrialState
|
||||
from optuna_dashboard.preferential._system_attrs import get_n_generate
|
||||
@@ -20,6 +20,10 @@ from optuna_dashboard.preferential._system_attrs import report_preferences
|
||||
from optuna_dashboard.preferential._system_attrs import set_n_generate
|
||||
|
||||
|
||||
with try_import() as _imports:
|
||||
from optuna_dashboard.preferential.samplers.gp import PreferentialGPSampler
|
||||
|
||||
|
||||
_logger = logging.get_logger(__name__)
|
||||
_SYSTEM_ATTR_PREFERENTIAL_STUDY = "preference:is_preferential"
|
||||
|
||||
@@ -344,11 +348,10 @@ def create_study(
|
||||
|
||||
sampler:
|
||||
A sampler object that implements background algorithm for value suggestion.
|
||||
If :obj:`None` is specified, `RandomSampler`_ is used. Please note that
|
||||
most Optuna samplers does not work efficiently for preferential optimization.
|
||||
|
||||
.. _RandomSampler: https://optuna.readthedocs.io/en/stable/reference/\
|
||||
samplers/generated/optuna.samplers.RandomSampler.html
|
||||
If :obj:`None` is specified,
|
||||
:class:`~optuna_dashboard.preferential.samplers.gp.PreferentialGPSampler` is used.
|
||||
Please note that most Optuna samplers does not work efficiently for preferential
|
||||
optimization.
|
||||
|
||||
study_name:
|
||||
Study's name. If this argument is set to None, a unique name is generated
|
||||
@@ -369,9 +372,13 @@ def create_study(
|
||||
The interface may change in newer versions without prior notice.
|
||||
"""
|
||||
try:
|
||||
if sampler is None:
|
||||
_imports.check() # If BoTorch is not installed, raise ImportError.
|
||||
sampler = PreferentialGPSampler()
|
||||
|
||||
study = optuna.create_study(
|
||||
storage=storage,
|
||||
sampler=sampler or RandomSampler(),
|
||||
sampler=sampler,
|
||||
study_name=study_name,
|
||||
)
|
||||
study._storage.set_study_system_attr(
|
||||
@@ -441,11 +448,10 @@ def load_study(
|
||||
:func:`~optuna.study.create_study` for further details.
|
||||
sampler:
|
||||
A sampler object that implements background algorithm for value suggestion.
|
||||
If :obj:`None` is specified, `RandomSampler`_ is used. Please note that
|
||||
most Optuna samplers does not work efficiently for preferential optimization.
|
||||
|
||||
.. _RandomSampler: https://optuna.readthedocs.io/en/stable/reference/samplers/\
|
||||
generated/optuna.samplers.RandomSampler.html
|
||||
If :obj:`None` is specified,
|
||||
:class:`~optuna_dashboard.preferential.samplers.gp.PreferentialGPSampler` is used.
|
||||
Please note that most Optuna samplers does not work efficiently for preferential
|
||||
optimization.
|
||||
|
||||
Returns:
|
||||
A :class:`~optuna_dashboard.preferential.PreferentialStudy` object.
|
||||
@@ -454,9 +460,11 @@ def load_study(
|
||||
Preferential optimization is an experimental feature (introduced in v0.13.0).
|
||||
The interface may change in newer versions without prior notice.
|
||||
"""
|
||||
study = optuna.load_study(
|
||||
study_name=study_name, storage=storage, sampler=sampler or RandomSampler()
|
||||
)
|
||||
if sampler is None:
|
||||
_imports.check() # If BoTorch is not installed, raise ImportError.
|
||||
sampler = PreferentialGPSampler()
|
||||
|
||||
study = optuna.load_study(study_name=study_name, storage=storage, sampler=sampler)
|
||||
system_attrs = study._storage.get_study_system_attrs(study._study_id)
|
||||
if not system_attrs.get(_SYSTEM_ATTR_PREFERENTIAL_STUDY):
|
||||
raise ValueError("The study is not a PreferentialStudy.")
|
||||
|
||||
@@ -50,15 +50,15 @@ def _orthants_MVN_Gibbs_sampling(cov_inv: Tensor, cycles: int, initial_sample: T
|
||||
|
||||
|
||||
def _one_side_trunc_norm_sampling(lower: Tensor) -> Tensor:
|
||||
if lower > 4.0:
|
||||
r = torch.clamp_min(torch.rand(torch.Size(()), dtype=torch.float64), min=1e-300)
|
||||
return (lower * lower - 2 * r.log()).sqrt()
|
||||
else:
|
||||
SQRT2 = math.sqrt(2)
|
||||
r = torch.rand(torch.Size(()), dtype=torch.float64) * torch.erfc(lower / SQRT2)
|
||||
while 1 - r == 1:
|
||||
r = torch.rand(torch.Size(()), dtype=torch.float64) * torch.erfc(lower / SQRT2)
|
||||
return torch.erfinv(1 - r) * SQRT2
|
||||
r = torch.rand(torch.Size(()), dtype=torch.float64)
|
||||
ret = -torch.special.ndtri(torch.exp(torch.special.log_ndtr(-lower) + r.log()))
|
||||
|
||||
# If sampled random number is very small, `ret` becomes inf.
|
||||
while torch.isinf(ret):
|
||||
r = torch.rand(torch.Size(()), dtype=torch.float64)
|
||||
ret = -torch.special.ndtri(torch.exp(torch.special.log_ndtr(-lower) + r.log()))
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
_orthants_MVN_Gibbs_sampling_jit = torch.jit.script(_orthants_MVN_Gibbs_sampling)
|
||||
@@ -284,6 +284,26 @@ class _PreferentialGP:
|
||||
|
||||
|
||||
class PreferentialGPSampler(optuna.samplers.BaseSampler):
|
||||
"""Sampler for preferential optimization using Gaussian process.
|
||||
|
||||
The sampling algorithm is based on `Takeno et al., 2023 <https://arxiv.org/abs/2302.01513>`_.
|
||||
This sampler uses BoTorch to optimize acquisition function.
|
||||
|
||||
Args:
|
||||
kernel:
|
||||
Kernel that computes the covariance on the Gaussian process. Defaults to
|
||||
Matern 3/2 Kernel + ARD.
|
||||
noise_prior:
|
||||
Prior of the observation noise. Defaults to gamma prior.
|
||||
independent_sampler:
|
||||
A :class:`~optuna.samplers.BaseSampler` instance that is used for independent
|
||||
sampling. The parameters not contained in the relative search space are sampled
|
||||
by this sampler. If :obj:`None` is specified,
|
||||
:class:`~optuna.samplers.RandomSampler` is used as the default.
|
||||
seed:
|
||||
Seed for random number generator.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -297,7 +317,7 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler):
|
||||
|
||||
self._rng = np.random.RandomState(seed)
|
||||
self.independent_sampler = independent_sampler or optuna.samplers.RandomSampler(
|
||||
seed=self._rng.randint(2**32)
|
||||
seed=self._rng.randint(2**32, dtype=np.int64)
|
||||
)
|
||||
|
||||
self._search_space = optuna.search_space.IntersectionSearchSpace()
|
||||
@@ -335,7 +355,7 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler):
|
||||
)
|
||||
pref_ids = torch.tensor([[ids[b], ids[w]] for b, w in preferences], dtype=torch.int32)
|
||||
with torch.random.fork_rng():
|
||||
torch.manual_seed(self._rng.randint(2**32))
|
||||
torch.manual_seed(self._rng.randint(2**32, dtype=np.int64))
|
||||
|
||||
self._gp = self._gp or _PreferentialGP(
|
||||
kernel=self.kernel
|
||||
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
tellTrialAPI,
|
||||
saveTrialUserAttrsAPI,
|
||||
renameStudyAPI,
|
||||
uploadArtifactAPI,
|
||||
uploadTrialArtifactAPI,
|
||||
uploadStudyArtifactAPI,
|
||||
getMetaInfoAPI,
|
||||
deleteArtifactAPI,
|
||||
deleteTrialArtifactAPI,
|
||||
deleteStudyArtifactAPI,
|
||||
reportPreferenceAPI,
|
||||
skipPreferentialTrialAPI,
|
||||
removePreferentialHistoryAPI,
|
||||
@@ -100,7 +102,13 @@ export const actionCreator = () => {
|
||||
setTrial(studyId, trialIndex, newTrial)
|
||||
}
|
||||
|
||||
const deleteTrialArtifact = (
|
||||
const setStudyArtifacts = (studyId: number, artifacts: Artifact[]) => {
|
||||
const newStudy: StudyDetail = Object.assign({}, studyDetails[studyId])
|
||||
newStudy.artifacts = artifacts
|
||||
setStudyDetailState(studyId, newStudy)
|
||||
}
|
||||
|
||||
const deleteTrialArtifactState = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
artifact_id: string
|
||||
@@ -122,6 +130,18 @@ export const actionCreator = () => {
|
||||
setTrialArtifacts(studyId, index, newArtifacts)
|
||||
}
|
||||
|
||||
const deleteStudyArtifactState = (studyId: number, artifact_id: string) => {
|
||||
const artifacts = studyDetails[studyId].artifacts
|
||||
const artifactIndex = artifacts.findIndex(
|
||||
(a) => a.artifact_id === artifact_id
|
||||
)
|
||||
const newArtifacts = [
|
||||
...artifacts.slice(0, artifactIndex),
|
||||
...artifacts.slice(artifactIndex + 1, artifacts.length),
|
||||
]
|
||||
setStudyArtifacts(studyId, newArtifacts)
|
||||
}
|
||||
|
||||
const setTrialStateValues = (
|
||||
studyId: number,
|
||||
index: number,
|
||||
@@ -154,7 +174,7 @@ export const actionCreator = () => {
|
||||
currentValue > bestValue
|
||||
) {
|
||||
newStudy.best_trials = [newTrial]
|
||||
} else if (currentValue == bestValue) {
|
||||
} else if (currentValue === bestValue) {
|
||||
newStudy.best_trials = [...newStudy.best_trials, newTrial]
|
||||
}
|
||||
}
|
||||
@@ -430,7 +450,7 @@ export const actionCreator = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const uploadArtifact = (
|
||||
const uploadTrialArtifact = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
file: File
|
||||
@@ -439,7 +459,7 @@ export const actionCreator = () => {
|
||||
setUploading(true)
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = (upload: ProgressEvent<FileReader>) => {
|
||||
uploadArtifactAPI(
|
||||
uploadTrialArtifactAPI(
|
||||
studyId,
|
||||
trialId,
|
||||
file.name,
|
||||
@@ -467,14 +487,56 @@ export const actionCreator = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteArtifact = (
|
||||
const uploadStudyArtifact = (studyId: number, file: File): void => {
|
||||
const reader = new FileReader()
|
||||
setUploading(true)
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = (upload: ProgressEvent<FileReader>) => {
|
||||
uploadStudyArtifactAPI(
|
||||
studyId,
|
||||
file.name,
|
||||
upload.target?.result as string
|
||||
)
|
||||
.then((res) => {
|
||||
setUploading(false)
|
||||
setStudyArtifacts(studyId, res.artifacts)
|
||||
})
|
||||
.catch((err) => {
|
||||
setUploading(false)
|
||||
const reason = err.response?.data.reason
|
||||
enqueueSnackbar(`Failed to upload ${reason}`, { variant: "error" })
|
||||
})
|
||||
}
|
||||
reader.onerror = (error) => {
|
||||
enqueueSnackbar(`Failed to read the file ${error}`, { variant: "error" })
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteTrialArtifact = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
artifactId: string
|
||||
): void => {
|
||||
deleteArtifactAPI(studyId, trialId, artifactId)
|
||||
deleteTrialArtifactAPI(studyId, trialId, artifactId)
|
||||
.then(() => {
|
||||
deleteTrialArtifact(studyId, trialId, artifactId)
|
||||
deleteTrialArtifactState(studyId, trialId, artifactId)
|
||||
enqueueSnackbar(`Success to delete an artifact.`, {
|
||||
variant: "success",
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
const reason = err.response?.data.reason
|
||||
enqueueSnackbar(`Failed to delete ${reason}.`, {
|
||||
variant: "error",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const deleteStudyArtifact = (studyId: number, artifactId: string): void => {
|
||||
deleteStudyArtifactAPI(studyId, artifactId)
|
||||
.then(() => {
|
||||
deleteStudyArtifactState(studyId, artifactId)
|
||||
enqueueSnackbar(`Success to delete an artifact.`, {
|
||||
variant: "success",
|
||||
})
|
||||
@@ -693,8 +755,10 @@ export const actionCreator = () => {
|
||||
saveReloadInterval,
|
||||
saveStudyNote,
|
||||
saveTrialNote,
|
||||
uploadArtifact,
|
||||
deleteArtifact,
|
||||
uploadTrialArtifact,
|
||||
uploadStudyArtifact,
|
||||
deleteTrialArtifact,
|
||||
deleteStudyArtifact,
|
||||
makeTrialComplete,
|
||||
makeTrialFail,
|
||||
saveTrialUserAttrs,
|
||||
|
||||
@@ -280,7 +280,7 @@ type UploadArtifactAPIResponse = {
|
||||
artifacts: Artifact[]
|
||||
}
|
||||
|
||||
export const uploadArtifactAPI = (
|
||||
export const uploadTrialArtifactAPI = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
fileName: string,
|
||||
@@ -296,7 +296,22 @@ export const uploadArtifactAPI = (
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteArtifactAPI = (
|
||||
export const uploadStudyArtifactAPI = (
|
||||
studyId: number,
|
||||
fileName: string,
|
||||
dataUrl: string
|
||||
): Promise<UploadArtifactAPIResponse> => {
|
||||
return axiosInstance
|
||||
.post<UploadArtifactAPIResponse>(`/api/artifacts/${studyId}`, {
|
||||
file: dataUrl,
|
||||
filename: fileName,
|
||||
})
|
||||
.then((res) => {
|
||||
return res.data
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteTrialArtifactAPI = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
artifactId: string
|
||||
@@ -308,6 +323,17 @@ export const deleteArtifactAPI = (
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteStudyArtifactAPI = (
|
||||
studyId: number,
|
||||
artifactId: string
|
||||
): Promise<void> => {
|
||||
return axiosInstance
|
||||
.delete<void>(`/api/artifacts/${studyId}/${artifactId}`)
|
||||
.then(() => {
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
export const tellTrialAPI = (
|
||||
trialId: number,
|
||||
state: TrialStateFinished,
|
||||
|
||||
@@ -3,8 +3,9 @@ import {
|
||||
ThreejsArtifactViewer,
|
||||
isThreejsArtifact,
|
||||
} from "./ThreejsArtifactViewer"
|
||||
import { WaveSurferArtifactViewer } from "./WaveSurferArtifactViewer"
|
||||
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"
|
||||
import { CardMedia } from "@mui/material"
|
||||
import { CardMedia, Box } from "@mui/material"
|
||||
|
||||
export const ArtifactCardMedia: FC<{
|
||||
artifact: Artifact
|
||||
@@ -35,9 +36,21 @@ export const ArtifactCardMedia: FC<{
|
||||
)
|
||||
} else if (artifact.mimetype.startsWith("audio")) {
|
||||
return (
|
||||
<audio controls>
|
||||
<source src={urlPath} type={artifact.mimetype} />
|
||||
</audio>
|
||||
<Box
|
||||
style={{
|
||||
width: "100%",
|
||||
height: height,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<WaveSurferArtifactViewer
|
||||
height={100}
|
||||
waveColor="rgb(200, 0, 200)"
|
||||
progressColor="rgb(100, 0, 100)"
|
||||
url={urlPath}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
} else if (artifact.mimetype.startsWith("image")) {
|
||||
return (
|
||||
|
||||
@@ -10,12 +10,16 @@ import {
|
||||
TableSortLabel,
|
||||
Collapse,
|
||||
IconButton,
|
||||
useTheme,
|
||||
Menu,
|
||||
MenuItem,
|
||||
} 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 CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"
|
||||
import CheckBoxIcon from "@mui/icons-material/CheckBox"
|
||||
import FilterListIcon from "@mui/icons-material/FilterList"
|
||||
import ListItemIcon from "@mui/material/ListItemIcon"
|
||||
|
||||
type Order = "asc" | "desc"
|
||||
|
||||
@@ -28,15 +32,15 @@ interface DataGridColumn<T> {
|
||||
field: keyof T
|
||||
label: string
|
||||
sortable?: boolean
|
||||
less?: (a: T, b: T) => number
|
||||
filterable?: boolean
|
||||
less?: (a: T, b: T, ascending: boolean) => number
|
||||
filterChoices?: string[]
|
||||
toCellValue?: (rowIndex: number) => string | React.ReactNode
|
||||
padding?: "normal" | "checkbox" | "none"
|
||||
}
|
||||
|
||||
interface RowFilter {
|
||||
columnIdx: number
|
||||
value: Value
|
||||
values: Value[]
|
||||
}
|
||||
|
||||
function DataGrid<T>(props: {
|
||||
@@ -81,28 +85,13 @@ function DataGrid<T>(props: {
|
||||
}
|
||||
|
||||
// Filtering
|
||||
const fieldAlreadyFiltered = (columnIdx: number): boolean =>
|
||||
filters.some((f) => f.columnIdx === columnIdx)
|
||||
|
||||
const handleClickFilterCell = (columnIdx: number, value: Value) => {
|
||||
if (fieldAlreadyFiltered(columnIdx)) {
|
||||
return
|
||||
}
|
||||
const newFilters = [...filters, { columnIdx: columnIdx, value: value }]
|
||||
setFilters(newFilters)
|
||||
}
|
||||
|
||||
const clearFilter = (columnIdx: number): void => {
|
||||
setFilters(filters.filter((f) => f.columnIdx !== columnIdx))
|
||||
}
|
||||
|
||||
const filteredRows = rows.filter((row, rowIdx) => {
|
||||
if (defaultFilter !== undefined && defaultFilter(row)) {
|
||||
return false
|
||||
}
|
||||
return filters.length === 0
|
||||
? true
|
||||
: filters.some((f) => {
|
||||
: filters.every((f) => {
|
||||
if (columns.length <= f.columnIdx) {
|
||||
console.log(
|
||||
`columnIdx=${f.columnIdx} must be smaller than columns.length=${columns.length}`
|
||||
@@ -110,20 +99,15 @@ function DataGrid<T>(props: {
|
||||
return true
|
||||
}
|
||||
const toCellValue = columns[f.columnIdx].toCellValue
|
||||
if (toCellValue !== undefined) {
|
||||
return toCellValue(rowIdx) === f.value
|
||||
}
|
||||
const field = columns[f.columnIdx].field
|
||||
return row[field] === f.value
|
||||
const cellValue =
|
||||
toCellValue !== undefined
|
||||
? toCellValue(rowIdx)
|
||||
: row[columns[f.columnIdx].field]
|
||||
return f.values.some((v) => v === cellValue)
|
||||
})
|
||||
})
|
||||
|
||||
// Sorting
|
||||
const createSortHandler = (columnId: number) => () => {
|
||||
const isAsc = orderBy === columnId && order === "asc"
|
||||
setOrder(isAsc ? "desc" : "asc")
|
||||
setOrderBy(columnId)
|
||||
}
|
||||
const sortedRows = stableSort<T>(filteredRows, order, orderBy, columns)
|
||||
const currentPageRows =
|
||||
rowsPerPage > 0
|
||||
@@ -135,20 +119,6 @@ function DataGrid<T>(props: {
|
||||
const RootDiv = styled("div")({
|
||||
width: "100%",
|
||||
})
|
||||
const HiddenSpan = styled("span")({
|
||||
border: 0,
|
||||
clip: "rect(0 0 0 0)",
|
||||
height: 1,
|
||||
margin: -1,
|
||||
overflow: "hidden",
|
||||
padding: 0,
|
||||
position: "absolute",
|
||||
top: 20,
|
||||
width: 1,
|
||||
})
|
||||
const TableHeaderCellSpan = styled("span")({
|
||||
display: "inline-flex",
|
||||
})
|
||||
return (
|
||||
<RootDiv>
|
||||
<TableContainer>
|
||||
@@ -160,50 +130,32 @@ function DataGrid<T>(props: {
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{collapseBody ? <TableCell /> : null}
|
||||
{columns.map((column, columnIdx) => (
|
||||
<TableCell
|
||||
key={columnIdx}
|
||||
padding={column.padding || "normal"}
|
||||
sortDirection={orderBy === column.field ? order : false}
|
||||
>
|
||||
<TableHeaderCellSpan>
|
||||
{column.sortable ? (
|
||||
<TableSortLabel
|
||||
active={orderBy === columnIdx}
|
||||
direction={orderBy === columnIdx ? order : "asc"}
|
||||
onClick={createSortHandler(columnIdx)}
|
||||
>
|
||||
{column.label}
|
||||
{orderBy === column.field ? (
|
||||
<HiddenSpan>
|
||||
{order === "desc"
|
||||
? "sorted descending"
|
||||
: "sorted ascending"}
|
||||
</HiddenSpan>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
) : (
|
||||
column.label
|
||||
)}
|
||||
{column.filterable ? (
|
||||
<IconButton
|
||||
size={dense ? "small" : "medium"}
|
||||
style={
|
||||
fieldAlreadyFiltered(columnIdx)
|
||||
? {}
|
||||
: { visibility: "hidden" }
|
||||
}
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
clearFilter(columnIdx)
|
||||
}}
|
||||
>
|
||||
<Clear />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</TableHeaderCellSpan>
|
||||
</TableCell>
|
||||
))}
|
||||
{columns.map((column, columnIdx) => {
|
||||
return (
|
||||
<DataGridHeaderColumn<T>
|
||||
key={columnIdx}
|
||||
column={column}
|
||||
order={orderBy === columnIdx ? order : null}
|
||||
filter={
|
||||
filters.find((f) => f.columnIdx === columnIdx) || null
|
||||
}
|
||||
onOrderByChange={(direction: Order) => {
|
||||
setOrder(direction)
|
||||
setOrderBy(columnIdx)
|
||||
}}
|
||||
onFilterChange={(values: Value[]) => {
|
||||
const newFilters = filters.filter(
|
||||
(f) => f.columnIdx !== columnIdx
|
||||
)
|
||||
newFilters.push({
|
||||
columnIdx: columnIdx,
|
||||
values: values,
|
||||
})
|
||||
setFilters(newFilters)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -215,7 +167,6 @@ function DataGrid<T>(props: {
|
||||
keyField={keyField}
|
||||
collapseBody={collapseBody}
|
||||
key={`${row[keyField]}`}
|
||||
handleClickFilterCell={handleClickFilterCell}
|
||||
/>
|
||||
))}
|
||||
{emptyRows > 0 && (
|
||||
@@ -239,30 +190,119 @@ function DataGrid<T>(props: {
|
||||
)
|
||||
}
|
||||
|
||||
const TableHeaderCellSpan = styled("span")({
|
||||
display: "inline-flex",
|
||||
})
|
||||
|
||||
const HiddenSpan = styled("span")({
|
||||
border: 0,
|
||||
clip: "rect(0 0 0 0)",
|
||||
height: 1,
|
||||
margin: -1,
|
||||
overflow: "hidden",
|
||||
padding: 0,
|
||||
position: "absolute",
|
||||
top: 20,
|
||||
width: 1,
|
||||
})
|
||||
|
||||
function DataGridHeaderColumn<T>(props: {
|
||||
column: DataGridColumn<T>
|
||||
order: Order | null
|
||||
onOrderByChange: (order: Order) => void
|
||||
filter: RowFilter | null
|
||||
onFilterChange: (values: Value[]) => void
|
||||
dense?: boolean
|
||||
}) {
|
||||
const { column, order, onOrderByChange, filter, onFilterChange, dense } =
|
||||
props
|
||||
const [filterMenuAnchorEl, setFilterMenuAnchorEl] =
|
||||
React.useState<null | HTMLElement>(null)
|
||||
|
||||
const filterChoices = column.filterChoices
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
padding={column.padding || "normal"}
|
||||
sortDirection={order !== null ? order : false}
|
||||
>
|
||||
<TableHeaderCellSpan>
|
||||
{column.sortable ? (
|
||||
<TableSortLabel
|
||||
active={order !== null}
|
||||
direction={order || "asc"}
|
||||
onClick={() => {
|
||||
onOrderByChange(order === "asc" ? "desc" : "asc")
|
||||
}}
|
||||
>
|
||||
{column.label}
|
||||
{order !== null ? (
|
||||
<HiddenSpan>
|
||||
{order === "desc" ? "sorted descending" : "sorted ascending"}
|
||||
</HiddenSpan>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
) : (
|
||||
column.label
|
||||
)}
|
||||
{filterChoices !== undefined ? (
|
||||
<>
|
||||
<IconButton
|
||||
size={dense ? "small" : "medium"}
|
||||
onClick={(e) => {
|
||||
setFilterMenuAnchorEl(e.currentTarget)
|
||||
}}
|
||||
>
|
||||
<FilterListIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={filterMenuAnchorEl}
|
||||
open={filterMenuAnchorEl !== null}
|
||||
onClose={() => {
|
||||
setFilterMenuAnchorEl(null)
|
||||
}}
|
||||
>
|
||||
{filterChoices.map((choice) => (
|
||||
<MenuItem
|
||||
key={choice}
|
||||
onClick={() => {
|
||||
const newTickedValues =
|
||||
filter === null
|
||||
? filterChoices.filter((v) => v !== choice) // By default, every choice is ticked, so the chosen option will be unticked.
|
||||
: filter.values.some((v) => v === choice)
|
||||
? filter.values.filter((v) => v !== choice)
|
||||
: [...filter.values, choice]
|
||||
onFilterChange(newTickedValues)
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
{!filter || filter.values.some((v) => v === choice) ? (
|
||||
<CheckBoxIcon color="primary" />
|
||||
) : (
|
||||
<CheckBoxOutlineBlankIcon color="primary" />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
{choice}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
) : null}
|
||||
</TableHeaderCellSpan>
|
||||
</TableCell>
|
||||
)
|
||||
}
|
||||
|
||||
function DataGridRow<T>(props: {
|
||||
columns: DataGridColumn<T>[]
|
||||
rowIndex: number
|
||||
row: T
|
||||
keyField: keyof T
|
||||
collapseBody?: (rowIndex: number) => React.ReactNode
|
||||
handleClickFilterCell: (columnIdx: number, value: Value) => void
|
||||
}) {
|
||||
const {
|
||||
columns,
|
||||
rowIndex,
|
||||
row,
|
||||
keyField,
|
||||
collapseBody,
|
||||
handleClickFilterCell,
|
||||
} = props
|
||||
const { columns, rowIndex, row, keyField, collapseBody } = props
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const theme = useTheme()
|
||||
|
||||
const FilterableDiv = styled("div")({
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
})
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableRow hover tabIndex={-1}>
|
||||
@@ -283,21 +323,7 @@ function DataGridRow<T>(props: {
|
||||
: // TODO(c-bata): Avoid this implicit type conversion.
|
||||
(row[column.field] as number | string | null | undefined)
|
||||
|
||||
return column.filterable ? (
|
||||
<TableCell
|
||||
key={`${row[keyField]}:${column.field.toString()}:${columnIndex}`}
|
||||
padding={column.padding || "normal"}
|
||||
onClick={() => {
|
||||
const value =
|
||||
column.toCellValue !== undefined
|
||||
? column.toCellValue(rowIndex)
|
||||
: row[column.field]
|
||||
handleClickFilterCell(columnIndex, value)
|
||||
}}
|
||||
>
|
||||
<FilterableDiv>{cellItem}</FilterableDiv>
|
||||
</TableCell>
|
||||
) : (
|
||||
return (
|
||||
<TableCell
|
||||
key={`${row[keyField]}:${column.field.toString()}:${columnIndex}`}
|
||||
padding={column.padding || "normal"}
|
||||
@@ -358,7 +384,10 @@ function stableSort<T>(
|
||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number])
|
||||
stabilizedThis.sort((a, b) => {
|
||||
if (less) {
|
||||
const result = order == "asc" ? -less(a[0], b[0]) : less(a[0], b[0])
|
||||
const ascending = order === "asc"
|
||||
const result = ascending
|
||||
? -less(a[0], b[0], ascending)
|
||||
: less(a[0], b[0], ascending)
|
||||
if (result !== 0) return result
|
||||
} else {
|
||||
const result = comparator(a[0], b[0])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactNode, useState } from "react"
|
||||
import React, { ReactNode, useState, FC } from "react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@mui/material"
|
||||
import { actionCreator } from "../action"
|
||||
|
||||
export const useDeleteArtifactDialog = (): [
|
||||
export const useDeleteTrialArtifactDialog = (): [
|
||||
(studyId: number, trialId: number, artifact: Artifact) => void,
|
||||
() => ReactNode
|
||||
] => {
|
||||
@@ -33,7 +33,7 @@ export const useDeleteArtifactDialog = (): [
|
||||
if (artifact === null) {
|
||||
return
|
||||
}
|
||||
action.deleteArtifact(studyId, trialId, artifact.artifact_id)
|
||||
action.deleteTrialArtifact(studyId, trialId, artifact.artifact_id)
|
||||
setOpenDeleteArtifactDialog(false)
|
||||
setTarget([-1, -1, null])
|
||||
}
|
||||
@@ -45,32 +45,96 @@ export const useDeleteArtifactDialog = (): [
|
||||
|
||||
const renderDeleteArtifactDialog = () => {
|
||||
return (
|
||||
<Dialog
|
||||
open={openDeleteArtifactDialog}
|
||||
onClose={() => {
|
||||
handleCloseDeleteArtifactDialog()
|
||||
}}
|
||||
aria-labelledby="delete-artifact-dialog-title"
|
||||
>
|
||||
<DialogTitle id="delete-artifact-dialog-title">
|
||||
Delete artifact
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Are you sure you want to delete an artifact ("
|
||||
{target[2]?.filename}")?
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDeleteArtifactDialog} color="primary">
|
||||
No
|
||||
</Button>
|
||||
<Button onClick={handleDeleteArtifact} color="primary">
|
||||
Yes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<DeleteDialog
|
||||
openDeleteArtifactDialog={openDeleteArtifactDialog}
|
||||
handleCloseDeleteArtifactDialog={handleCloseDeleteArtifactDialog}
|
||||
filename={target[2]?.filename}
|
||||
handleDeleteArtifact={handleDeleteArtifact}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return [openDialog, renderDeleteArtifactDialog]
|
||||
}
|
||||
|
||||
export const useDeleteStudyArtifactDialog = (): [
|
||||
(studyId: number, artifact: Artifact) => void,
|
||||
() => ReactNode
|
||||
] => {
|
||||
const action = actionCreator()
|
||||
|
||||
const [openDeleteArtifactDialog, setOpenDeleteArtifactDialog] =
|
||||
useState(false)
|
||||
const [target, setTarget] = useState<[number, Artifact | null]>([-1, null])
|
||||
|
||||
const handleCloseDeleteArtifactDialog = () => {
|
||||
setOpenDeleteArtifactDialog(false)
|
||||
setTarget([-1, null])
|
||||
}
|
||||
|
||||
const handleDeleteArtifact = () => {
|
||||
const [studyId, artifact] = target
|
||||
if (artifact === null) {
|
||||
return
|
||||
}
|
||||
action.deleteStudyArtifact(studyId, artifact.artifact_id)
|
||||
setOpenDeleteArtifactDialog(false)
|
||||
setTarget([-1, null])
|
||||
}
|
||||
|
||||
const openDialog = (studyId: number, artifact: Artifact) => {
|
||||
setTarget([studyId, artifact])
|
||||
setOpenDeleteArtifactDialog(true)
|
||||
}
|
||||
|
||||
const renderDeleteArtifactDialog = () => {
|
||||
return (
|
||||
<DeleteDialog
|
||||
openDeleteArtifactDialog={openDeleteArtifactDialog}
|
||||
handleCloseDeleteArtifactDialog={handleCloseDeleteArtifactDialog}
|
||||
filename={target[1]?.filename}
|
||||
handleDeleteArtifact={handleDeleteArtifact}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return [openDialog, renderDeleteArtifactDialog]
|
||||
}
|
||||
|
||||
const DeleteDialog: FC<{
|
||||
openDeleteArtifactDialog: boolean
|
||||
handleCloseDeleteArtifactDialog: () => void
|
||||
filename: string | undefined
|
||||
handleDeleteArtifact: () => void
|
||||
}> = ({
|
||||
openDeleteArtifactDialog,
|
||||
handleCloseDeleteArtifactDialog,
|
||||
filename,
|
||||
handleDeleteArtifact,
|
||||
}) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={openDeleteArtifactDialog}
|
||||
onClose={() => {
|
||||
handleCloseDeleteArtifactDialog()
|
||||
}}
|
||||
aria-labelledby="delete-artifact-dialog-title"
|
||||
>
|
||||
<DialogTitle id="delete-artifact-dialog-title">
|
||||
Delete artifact
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Are you sure you want to delete an artifact ("
|
||||
{filename}")?
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDeleteArtifactDialog} color="primary">
|
||||
No
|
||||
</Button>
|
||||
<Button onClick={handleDeleteArtifact} color="primary">
|
||||
Yes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,25 +14,8 @@ import {
|
||||
import blue from "@mui/material/colors/blue"
|
||||
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
|
||||
import { useMergedUnionSearchSpace } from "../searchSpace"
|
||||
import { getAxisInfo } from "../graphUtil"
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const unique = (array: any[]) => {
|
||||
const knownElements = new Map()
|
||||
array.forEach((elem) => knownElements.set(elem, true))
|
||||
return Array.from(knownElements.keys())
|
||||
}
|
||||
|
||||
type AxisInfo = {
|
||||
name: string
|
||||
min: number
|
||||
max: number
|
||||
isLog: boolean
|
||||
isCat: boolean
|
||||
indices: (string | number)[]
|
||||
values: (string | number | null)[]
|
||||
}
|
||||
|
||||
const PADDING_RATIO = 0.05
|
||||
const plotDomId = "graph-contour"
|
||||
|
||||
export const Contour: FC<{
|
||||
@@ -195,12 +178,16 @@ const plotContour = (
|
||||
const xValues: plotly.Datum[] = []
|
||||
const yValues: plotly.Datum[] = []
|
||||
const zValues: plotly.Datum[][] = new Array(yIndices.length)
|
||||
const feasibleXY = new Set<number>()
|
||||
for (let j = 0; j < yIndices.length; j++) {
|
||||
zValues[j] = new Array(xIndices.length).fill(null)
|
||||
}
|
||||
|
||||
filteredTrials.forEach((trial, i) => {
|
||||
if (xAxis.values[i] && yAxis.values[i] && trial.values) {
|
||||
if (trial.constraints.every((c) => c <= 0)) {
|
||||
feasibleXY.add(xValues.length)
|
||||
}
|
||||
const xValue = xAxis.values[i] as string | number
|
||||
const yValue = yAxis.values[i] as string | number
|
||||
xValues.push(xValue)
|
||||
@@ -234,12 +221,20 @@ const plotContour = (
|
||||
},
|
||||
{
|
||||
type: "scatter",
|
||||
x: xValues,
|
||||
y: yValues,
|
||||
x: xValues.filter((_, i) => feasibleXY.has(i)),
|
||||
y: yValues.filter((_, i) => feasibleXY.has(i)),
|
||||
marker: { line: { width: 2.0, color: "Grey" }, color: "black" },
|
||||
mode: "markers",
|
||||
showlegend: false,
|
||||
},
|
||||
{
|
||||
type: "scatter",
|
||||
x: xValues.filter((_, i) => !feasibleXY.has(i)),
|
||||
y: yValues.filter((_, i) => !feasibleXY.has(i)),
|
||||
marker: { line: { width: 2.0, color: "Grey" }, color: "#cccccc" },
|
||||
mode: "markers",
|
||||
showlegend: false,
|
||||
},
|
||||
]
|
||||
plotly.react(plotDomId, plotData, layout)
|
||||
return
|
||||
@@ -272,93 +267,3 @@ const plotContour = (
|
||||
]
|
||||
plotly.react(plotDomId, plotData, layout)
|
||||
}
|
||||
|
||||
const getAxisInfoForNumericalParams = (
|
||||
trials: Trial[],
|
||||
paramName: string,
|
||||
distribution: FloatDistribution | IntDistribution
|
||||
): AxisInfo => {
|
||||
let min = 0
|
||||
let max = 0
|
||||
if (distribution.log) {
|
||||
const padding =
|
||||
(Math.log10(distribution.high) - Math.log10(distribution.low)) *
|
||||
PADDING_RATIO
|
||||
min = Math.pow(10, Math.log10(distribution.low) - padding)
|
||||
max = Math.pow(10, Math.log10(distribution.high) + padding)
|
||||
} else {
|
||||
const padding = (distribution.high - distribution.low) * PADDING_RATIO
|
||||
min = distribution.low - padding
|
||||
max = distribution.high + padding
|
||||
}
|
||||
|
||||
const values = trials.map(
|
||||
(trial) =>
|
||||
trial.params.find((p) => p.name === paramName)?.param_internal_value ||
|
||||
null
|
||||
)
|
||||
const indices = unique(values)
|
||||
.filter((v) => v !== null)
|
||||
.sort((a, b) => a - b)
|
||||
if (indices.length >= 2) {
|
||||
indices.unshift(min)
|
||||
indices.push(max)
|
||||
}
|
||||
return {
|
||||
name: paramName,
|
||||
min,
|
||||
max,
|
||||
isLog: distribution.log,
|
||||
isCat: false,
|
||||
indices,
|
||||
values,
|
||||
}
|
||||
}
|
||||
|
||||
const getAxisInfoForCategoricalParams = (
|
||||
trials: Trial[],
|
||||
paramName: string,
|
||||
distribution: CategoricalDistribution
|
||||
): AxisInfo => {
|
||||
const values = trials.map(
|
||||
(trial) =>
|
||||
trial.params.find((p) => p.name === paramName)?.param_external_value ||
|
||||
null
|
||||
)
|
||||
const isDynamic = values.some((v) => v === null)
|
||||
const span = distribution.choices.length - (isDynamic ? 2 : 1)
|
||||
const padding = span * PADDING_RATIO
|
||||
const min = -padding
|
||||
const max = span + padding
|
||||
|
||||
const indices = distribution.choices
|
||||
.map((c) => c.value)
|
||||
.sort((a, b) =>
|
||||
a.toLowerCase() < b.toLowerCase()
|
||||
? -1
|
||||
: a.toLowerCase() > b.toLowerCase()
|
||||
? 1
|
||||
: 0
|
||||
)
|
||||
return {
|
||||
name: paramName,
|
||||
min,
|
||||
max,
|
||||
isLog: false,
|
||||
isCat: true,
|
||||
indices,
|
||||
values,
|
||||
}
|
||||
}
|
||||
|
||||
const getAxisInfo = (trials: Trial[], param: SearchSpaceItem): AxisInfo => {
|
||||
if (param.distribution.type === "CategoricalDistribution") {
|
||||
return getAxisInfoForCategoricalParams(
|
||||
trials,
|
||||
param.name,
|
||||
param.distribution
|
||||
)
|
||||
} else {
|
||||
return getAxisInfoForNumericalParams(trials, param.name, param.distribution)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,22 +78,27 @@ const plotIntermediateValue = (
|
||||
t.state === "Pruned" &&
|
||||
t.values &&
|
||||
t.values.length > 0) ||
|
||||
t.state == "Running"
|
||||
t.state === "Running"
|
||||
)
|
||||
const plotData: Partial<plotly.PlotData>[] = filteredTrials.map((trial) => {
|
||||
const values = trial.intermediate_values.filter(
|
||||
(iv) => iv.value !== "inf" && iv.value !== "-inf" && iv.value !== "nan"
|
||||
)
|
||||
const isFeasible = trial.constraints.every((c) => c <= 0)
|
||||
return {
|
||||
x: values.map((iv) => iv.step),
|
||||
y: values.map((iv) => iv.value),
|
||||
marker: { maxdisplayed: 10 },
|
||||
mode: "lines+markers",
|
||||
type: "scatter",
|
||||
name:
|
||||
trial.state !== "Running"
|
||||
? `trial #${trial.number}`
|
||||
: `trial #${trial.number} (running)`,
|
||||
name: `trial #${trial.number} ${
|
||||
trial.state === "Running"
|
||||
? "(running)"
|
||||
: !isFeasible
|
||||
? "(infeasible)"
|
||||
: ""
|
||||
}`,
|
||||
...(!isFeasible && { line: { color: "#CCCCCC" } }),
|
||||
}
|
||||
})
|
||||
plotly.react(plotDomId, plotData, layout)
|
||||
|
||||
@@ -164,7 +164,7 @@ const plotCoordinate = (
|
||||
return truncated
|
||||
.split("")
|
||||
.map((c, i) => {
|
||||
return (i + 1) % breakLength == 0 ? c + "<br>" : c
|
||||
return (i + 1) % breakLength === 0 ? c + "<br>" : c
|
||||
})
|
||||
.join("")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import React, { FC, useEffect, useState } from "react"
|
||||
import {
|
||||
Grid,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Typography,
|
||||
SelectChangeEvent,
|
||||
useTheme,
|
||||
Box,
|
||||
} from "@mui/material"
|
||||
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
|
||||
import { getAxisInfo, makeHovertext } from "../graphUtil"
|
||||
import { useMergedUnionSearchSpace } from "../searchSpace"
|
||||
|
||||
const plotDomId = "graph-rank"
|
||||
|
||||
interface RankPlotInfo {
|
||||
xtitle: string
|
||||
ytitle: string
|
||||
xtype: plotly.AxisType
|
||||
ytype: plotly.AxisType
|
||||
xvalues: (string | number)[]
|
||||
yvalues: (string | number)[]
|
||||
colors: number[]
|
||||
is_feasible: boolean[]
|
||||
hovertext: string[]
|
||||
}
|
||||
|
||||
export const GraphRank: FC<{
|
||||
study: StudyDetail | null
|
||||
}> = ({ study = null }) => {
|
||||
const theme = useTheme()
|
||||
const [objectiveId, setobjectiveId] = useState<number>(0)
|
||||
const searchSpace = useMergedUnionSearchSpace(study?.union_search_space)
|
||||
const [xParam, setXParam] = useState<SearchSpaceItem | null>(null)
|
||||
const [yParam, setYParam] = useState<SearchSpaceItem | null>(null)
|
||||
const objectiveNames: string[] = study?.objective_names || []
|
||||
|
||||
if (xParam === null && searchSpace.length > 0) {
|
||||
setXParam(searchSpace[0])
|
||||
}
|
||||
if (yParam === null && searchSpace.length > 1) {
|
||||
setYParam(searchSpace[1])
|
||||
}
|
||||
|
||||
const handleObjectiveChange = (event: SelectChangeEvent<number>) => {
|
||||
setobjectiveId(Number(event.target.value))
|
||||
}
|
||||
const handleXParamChange = (event: SelectChangeEvent<string>) => {
|
||||
const param = searchSpace.find((item) => item.name === event.target.value)
|
||||
setXParam(param || null)
|
||||
}
|
||||
const handleYParamChange = (event: SelectChangeEvent<string>) => {
|
||||
const param = searchSpace.find((item) => item.name === event.target.value)
|
||||
setYParam(param || null)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (study != null) {
|
||||
const rankPlotInfo = getRankPlotInfo(study, objectiveId, xParam, yParam)
|
||||
plotRank(rankPlotInfo, theme.palette.mode)
|
||||
}
|
||||
}, [study, objectiveId, xParam, yParam, theme.palette.mode])
|
||||
|
||||
const space: SearchSpaceItem[] = study ? study.union_search_space : []
|
||||
|
||||
return (
|
||||
<Grid container direction="row">
|
||||
<Grid
|
||||
item
|
||||
xs={3}
|
||||
container
|
||||
direction="column"
|
||||
sx={{ paddingRight: theme.spacing(2) }}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
|
||||
>
|
||||
Rank
|
||||
</Typography>
|
||||
{study !== null && study.directions.length !== 1 ? (
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Objective:</FormLabel>
|
||||
<Select value={objectiveId} onChange={handleObjectiveChange}>
|
||||
{study.directions.map((d, i) => (
|
||||
<MenuItem value={i} key={i}>
|
||||
{objectiveNames.length === study?.directions.length
|
||||
? objectiveNames[i]
|
||||
: `${i}`}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
) : null}
|
||||
{study !== null && space.length > 0 ? (
|
||||
<Grid container direction="column" gap={1}>
|
||||
<FormControl component="fieldset" fullWidth>
|
||||
<FormLabel component="legend">x:</FormLabel>
|
||||
<Select value={xParam?.name || ""} onChange={handleXParamChange}>
|
||||
{space.map((d) => (
|
||||
<MenuItem value={d.name} key={d.name}>
|
||||
{d.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl component="fieldset" fullWidth>
|
||||
<FormLabel component="legend">y:</FormLabel>
|
||||
<Select value={yParam?.name || ""} onChange={handleYParamChange}>
|
||||
{space.map((d) => (
|
||||
<MenuItem value={d.name} key={d.name}>
|
||||
{d.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
) : null}
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<Box id={plotDomId} sx={{ height: "450px" }} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const getRankPlotInfo = (
|
||||
study: StudyDetail | null,
|
||||
objectiveId: number,
|
||||
xParam: SearchSpaceItem | null,
|
||||
yParam: SearchSpaceItem | null
|
||||
): RankPlotInfo | null => {
|
||||
if (study === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const trials = study.trials
|
||||
const filteredTrials = trials.filter(filterFunc)
|
||||
if (filteredTrials.length < 2 || xParam === null || yParam === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const xAxis = getAxisInfo(filteredTrials, xParam)
|
||||
const yAxis = getAxisInfo(filteredTrials, yParam)
|
||||
|
||||
let xValues: (string | number)[] = []
|
||||
let yValues: (string | number)[] = []
|
||||
const zValues: number[] = []
|
||||
const isFeasible: boolean[] = []
|
||||
const hovertext: string[] = []
|
||||
const convertTrialValueToNumber = (value: TrialValueNumber): number => {
|
||||
// TrialValueNumber takes `number`, "inf", or "-inf".
|
||||
return typeof value === "number"
|
||||
? value
|
||||
: value.includes("-")
|
||||
? -Infinity
|
||||
: Infinity
|
||||
}
|
||||
filteredTrials.forEach((trial, i) => {
|
||||
const xValue = xAxis.values[i]
|
||||
const yValue = yAxis.values[i]
|
||||
if (xValue && yValue && trial.values) {
|
||||
xValues.push(xValue)
|
||||
yValues.push(yValue)
|
||||
const zValue = convertTrialValueToNumber(trial.values[objectiveId])
|
||||
zValues.push(zValue)
|
||||
const feasibility = trial.constraints.every((c) => c <= 0)
|
||||
isFeasible.push(feasibility)
|
||||
hovertext.push(makeHovertext(trial))
|
||||
}
|
||||
})
|
||||
|
||||
const colors = getColors(zValues)
|
||||
|
||||
if (xAxis.isCat && !yAxis.isCat) {
|
||||
const indices: number[] = Array.from(Array(xValues.length).keys()).sort(
|
||||
(a, b) =>
|
||||
xValues[a]
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.localeCompare(xValues[b].toString().toLowerCase())
|
||||
)
|
||||
xValues = indices.map((i) => xValues[i])
|
||||
yValues = indices.map((i) => yValues[i])
|
||||
} else if (!xAxis.isCat && yAxis.isCat) {
|
||||
const indices: number[] = Array.from(Array(yValues.length).keys()).sort(
|
||||
(a, b) =>
|
||||
yValues[a]
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.localeCompare(yValues[b].toString().toLowerCase())
|
||||
)
|
||||
xValues = indices.map((i) => xValues[i])
|
||||
yValues = indices.map((i) => yValues[i])
|
||||
} else if (xAxis.isCat && yAxis.isCat) {
|
||||
const indices: number[] = Array.from(Array(xValues.length).keys()).sort(
|
||||
(a, b) => {
|
||||
const xComp = xValues[a]
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.localeCompare(xValues[b].toString().toLowerCase())
|
||||
if (xComp !== 0) {
|
||||
return xComp
|
||||
}
|
||||
return yValues[a]
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.localeCompare(yValues[b].toString().toLowerCase())
|
||||
}
|
||||
)
|
||||
xValues = indices.map((i) => xValues[i])
|
||||
yValues = indices.map((i) => yValues[i])
|
||||
}
|
||||
|
||||
return {
|
||||
xtitle: xAxis.name,
|
||||
ytitle: yAxis.name,
|
||||
xtype: xAxis.isCat ? "category" : xAxis.isLog ? "log" : "linear",
|
||||
ytype: yAxis.isCat ? "category" : yAxis.isLog ? "log" : "linear",
|
||||
xvalues: xValues,
|
||||
yvalues: yValues,
|
||||
colors,
|
||||
is_feasible: isFeasible,
|
||||
hovertext,
|
||||
}
|
||||
}
|
||||
|
||||
const filterFunc = (trial: Trial): boolean => {
|
||||
return trial.state === "Complete" && trial.values !== undefined
|
||||
}
|
||||
|
||||
const getColors = (values: number[]): number[] => {
|
||||
const rawRanks = getOrderWithSameOrderAveraging(values)
|
||||
let colorIdxs: number[] = []
|
||||
if (values.length > 2) {
|
||||
colorIdxs = rawRanks.map((rank) => rank / (values.length - 1))
|
||||
} else {
|
||||
colorIdxs = [0.5]
|
||||
}
|
||||
return colorIdxs
|
||||
}
|
||||
|
||||
const getOrderWithSameOrderAveraging = (values: number[]): number[] => {
|
||||
const sortedValues = values.slice().sort((a, b) => a - b)
|
||||
const ranks: number[] = []
|
||||
values.forEach((value) => {
|
||||
const firstIndex = sortedValues.indexOf(value)
|
||||
const lastIndex = sortedValues.lastIndexOf(value)
|
||||
const sumOfTheValue =
|
||||
((firstIndex + lastIndex) * (lastIndex - firstIndex + 1)) / 2
|
||||
const rank = sumOfTheValue / (lastIndex - firstIndex + 1)
|
||||
ranks.push(rank)
|
||||
})
|
||||
return ranks
|
||||
}
|
||||
|
||||
const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => {
|
||||
if (document.getElementById(plotDomId) === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (rankPlotInfo === null) {
|
||||
plotly.react(plotDomId, [], {
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const layout: Partial<plotly.Layout> = {
|
||||
xaxis: {
|
||||
title: rankPlotInfo.xtitle,
|
||||
type: rankPlotInfo.xtype,
|
||||
},
|
||||
yaxis: {
|
||||
title: rankPlotInfo.ytitle,
|
||||
type: rankPlotInfo.ytype,
|
||||
},
|
||||
margin: {
|
||||
l: 50,
|
||||
t: 0,
|
||||
r: 50,
|
||||
b: 50,
|
||||
},
|
||||
uirevision: "true",
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
}
|
||||
|
||||
const xValues = rankPlotInfo.xvalues
|
||||
const yValues = rankPlotInfo.yvalues
|
||||
|
||||
const plotData: Partial<plotly.PlotData>[] = [
|
||||
{
|
||||
type: "scatter",
|
||||
x: xValues.filter((_, i) => rankPlotInfo.is_feasible[i]),
|
||||
y: yValues.filter((_, i) => rankPlotInfo.is_feasible[i]),
|
||||
marker: {
|
||||
color: rankPlotInfo.colors.filter(
|
||||
(_, i) => rankPlotInfo.is_feasible[i]
|
||||
),
|
||||
colorscale: "Portland",
|
||||
colorbar: {
|
||||
title: "Rank",
|
||||
},
|
||||
size: 10,
|
||||
line: {
|
||||
color: "Grey",
|
||||
width: 0.5,
|
||||
},
|
||||
},
|
||||
mode: "markers",
|
||||
showlegend: false,
|
||||
hovertemplate: "%{hovertext}<extra></extra>",
|
||||
hovertext: rankPlotInfo.hovertext.filter(
|
||||
(_, i) => rankPlotInfo.is_feasible[i]
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "scatter",
|
||||
x: xValues.filter((_, i) => !rankPlotInfo.is_feasible[i]),
|
||||
y: yValues.filter((_, i) => !rankPlotInfo.is_feasible[i]),
|
||||
marker: {
|
||||
color: "#cccccc",
|
||||
size: 10,
|
||||
line: {
|
||||
color: "Grey",
|
||||
width: 0.5,
|
||||
},
|
||||
},
|
||||
mode: "markers",
|
||||
showlegend: false,
|
||||
hovertemplate: "%{hovertext}<extra></extra>",
|
||||
hovertext: rankPlotInfo.hovertext.filter(
|
||||
(_, i) => !rankPlotInfo.is_feasible[i]
|
||||
),
|
||||
},
|
||||
]
|
||||
plotly.react(plotDomId, plotData, layout)
|
||||
}
|
||||
@@ -188,65 +188,71 @@ const plotSlice = (
|
||||
return
|
||||
}
|
||||
|
||||
const objectiveValues: number[] = trials.map(
|
||||
const feasibleTrials: Trial[] = []
|
||||
const infeasibleTrials: Trial[] = []
|
||||
trials.forEach((t) => {
|
||||
if (t.constraints.every((c) => c <= 0)) {
|
||||
feasibleTrials.push(t)
|
||||
} else {
|
||||
infeasibleTrials.push(t)
|
||||
}
|
||||
})
|
||||
|
||||
const feasibleObjectiveValues: number[] = feasibleTrials.map(
|
||||
(t) => objectiveTarget.getTargetValue(t) as number
|
||||
)
|
||||
const values = trials.map(
|
||||
(t) => selectedParamTarget.getTargetValue(t) as number
|
||||
const infeasibleObjectiveValues: number[] = infeasibleTrials.map(
|
||||
(t) => objectiveTarget.getTargetValue(t) as number
|
||||
)
|
||||
|
||||
const trialNumbers: number[] = trials.map((t) => t.number)
|
||||
if (selectedParamSpace.distribution.type !== "CategoricalDistribution") {
|
||||
const trace: plotly.Data[] = [
|
||||
{
|
||||
type: "scatter",
|
||||
x: values,
|
||||
y: objectiveValues,
|
||||
mode: "markers",
|
||||
marker: {
|
||||
color: trialNumbers,
|
||||
colorscale: "Blues",
|
||||
reversescale: true,
|
||||
colorbar: {
|
||||
title: "Trial",
|
||||
},
|
||||
line: {
|
||||
color: "Grey",
|
||||
width: 0.5,
|
||||
},
|
||||
const feasibleValues = feasibleTrials.map(
|
||||
(t) => selectedParamTarget.getTargetValue(t) as number
|
||||
)
|
||||
const infeasibleValues = infeasibleTrials.map(
|
||||
(t) => selectedParamTarget.getTargetValue(t) as number
|
||||
)
|
||||
const trace: plotly.Data[] = [
|
||||
{
|
||||
type: "scatter",
|
||||
x: feasibleValues,
|
||||
y: feasibleObjectiveValues,
|
||||
mode: "markers",
|
||||
name: "Feasible Trial",
|
||||
marker: {
|
||||
color: feasibleTrials.map((t) => t.number),
|
||||
colorscale: "Blues",
|
||||
reversescale: true,
|
||||
colorbar: {
|
||||
title: "Trial",
|
||||
},
|
||||
line: {
|
||||
color: "Grey",
|
||||
width: 0.5,
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "scatter",
|
||||
x: infeasibleValues,
|
||||
y: infeasibleObjectiveValues,
|
||||
mode: "markers",
|
||||
name: "Infeasible Trial",
|
||||
marker: {
|
||||
color: "#cccccc",
|
||||
reversescale: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
if (selectedParamSpace.distribution.type !== "CategoricalDistribution") {
|
||||
layout["xaxis"] = {
|
||||
title: selectedParamTarget.toLabel(),
|
||||
type: isLogScale(selectedParamSpace) ? "log" : "linear",
|
||||
gridwidth: 1,
|
||||
automargin: true, // Otherwise the label is outside of the plot
|
||||
}
|
||||
plotly.react(plotDomId, trace, layout)
|
||||
} else {
|
||||
const vocabArr = selectedParamSpace.distribution.choices.map((c) => c.value)
|
||||
const tickvals: number[] = vocabArr.map((v, i) => i)
|
||||
const trace: plotly.Data[] = [
|
||||
{
|
||||
type: "scatter",
|
||||
x: values,
|
||||
y: objectiveValues,
|
||||
mode: "markers",
|
||||
marker: {
|
||||
color: trialNumbers,
|
||||
colorscale: "Blues",
|
||||
reversescale: true,
|
||||
colorbar: {
|
||||
title: "Trial",
|
||||
},
|
||||
line: {
|
||||
color: "Grey",
|
||||
width: 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
layout["xaxis"] = {
|
||||
title: selectedParamTarget.toLabel(),
|
||||
type: "linear",
|
||||
@@ -255,6 +261,6 @@ const plotSlice = (
|
||||
ticktext: vocabArr,
|
||||
automargin: true, // Otherwise the label is outside of the plot
|
||||
}
|
||||
plotly.react(plotDomId, trace, layout)
|
||||
}
|
||||
plotly.react(plotDomId, trace, layout)
|
||||
}
|
||||
|
||||
@@ -92,12 +92,7 @@ const plotTimeline = (trials: Trial[], mode: string) => {
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
}
|
||||
|
||||
const traces: Partial<plotly.PlotData>[] = []
|
||||
for (const s of Object.keys(cm) as TrialState[]) {
|
||||
const bars = trials.filter((t) => t.state === s)
|
||||
if (bars.length === 0) {
|
||||
continue
|
||||
}
|
||||
const makeTrace = (bars: Trial[], state: string, color: string) => {
|
||||
const starts = bars.map((b) => b.datetime_start ?? new Date())
|
||||
const completes = bars.map((b, i) => b.datetime_complete ?? starts[i])
|
||||
const trace: Partial<plotly.PlotData> = {
|
||||
@@ -106,14 +101,38 @@ const plotTimeline = (trials: Trial[], mode: string) => {
|
||||
y: bars.map((b) => b.number),
|
||||
// @ts-ignore: To suppress ts(2322)
|
||||
base: starts.map((s) => s.toISOString()),
|
||||
name: s,
|
||||
name: state,
|
||||
text: bars.map((b) => makeHovertext(b)),
|
||||
hovertemplate: "%{text}<extra>" + s + "</extra>",
|
||||
hovertemplate: "%{text}<extra>" + state + "</extra>",
|
||||
orientation: "h",
|
||||
marker: { color: cm[s] },
|
||||
marker: { color: color },
|
||||
textposition: "none", // Avoid drawing hovertext in a bar.
|
||||
}
|
||||
traces.push(trace)
|
||||
return trace
|
||||
}
|
||||
|
||||
const traces: Partial<plotly.PlotData>[] = []
|
||||
for (const [state, color] of Object.entries(cm)) {
|
||||
const bars = trials.filter((t) => t.state === state)
|
||||
if (bars.length === 0) {
|
||||
continue
|
||||
}
|
||||
if (state === "Complete") {
|
||||
const feasibleTrials = bars.filter((t) =>
|
||||
t.constraints.every((c) => c <= 0)
|
||||
)
|
||||
const infeasibleTrials = bars.filter((t) =>
|
||||
t.constraints.some((c) => c > 0)
|
||||
)
|
||||
if (feasibleTrials.length > 0) {
|
||||
traces.push(makeTrace(feasibleTrials, "Complete", color))
|
||||
}
|
||||
if (infeasibleTrials.length > 0) {
|
||||
traces.push(makeTrace(infeasibleTrials, "Infeasible", "#cccccc"))
|
||||
}
|
||||
} else {
|
||||
traces.push(makeTrace(bars, state, color))
|
||||
}
|
||||
}
|
||||
plotly.react(plotDomId, traces, layout)
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ const ArtifactUploader: FC<{
|
||||
if (files === null) {
|
||||
return
|
||||
}
|
||||
action.uploadArtifact(studyId, trialId, files[0])
|
||||
action.uploadTrialArtifact(studyId, trialId, files[0])
|
||||
}
|
||||
|
||||
const handleDrop: DragEventHandler = (e) => {
|
||||
@@ -433,7 +433,7 @@ const ArtifactUploader: FC<{
|
||||
e.preventDefault()
|
||||
const file = e.dataTransfer.files[0]
|
||||
setDragOver(false)
|
||||
action.uploadArtifact(studyId, trialId, file)
|
||||
action.uploadTrialArtifact(studyId, trialId, file)
|
||||
}
|
||||
|
||||
const handleDragOver: DragEventHandler = (e) => {
|
||||
|
||||
@@ -147,6 +147,16 @@ const CandidateTrial: FC<{
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: theme.spacing(2),
|
||||
right: theme.spacing(2),
|
||||
}}
|
||||
onClick={() => setDetailShown(false)}
|
||||
>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
<TrialListDetail
|
||||
trial={trial}
|
||||
isBestTrial={() => false}
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import React, {
|
||||
ChangeEventHandler,
|
||||
DragEventHandler,
|
||||
FC,
|
||||
MouseEventHandler,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
useTheme,
|
||||
IconButton,
|
||||
Card,
|
||||
CardContent,
|
||||
CardActionArea,
|
||||
} from "@mui/material"
|
||||
import UploadFileIcon from "@mui/icons-material/UploadFile"
|
||||
import DownloadIcon from "@mui/icons-material/Download"
|
||||
import DeleteIcon from "@mui/icons-material/Delete"
|
||||
import FullscreenIcon from "@mui/icons-material/Fullscreen"
|
||||
|
||||
import { actionCreator } from "../action"
|
||||
import { useDeleteStudyArtifactDialog } from "./DeleteArtifactDialog"
|
||||
import {
|
||||
useThreejsArtifactModal,
|
||||
isThreejsArtifact,
|
||||
} from "./ThreejsArtifactViewer"
|
||||
import { ArtifactCardMedia } from "./ArtifactCardMedia"
|
||||
|
||||
export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => {
|
||||
const theme = useTheme()
|
||||
const [openDeleteArtifactDialog, renderDeleteArtifactDialog] =
|
||||
useDeleteStudyArtifactDialog()
|
||||
const [openThreejsArtifactModal, renderThreejsArtifactModal] =
|
||||
useThreejsArtifactModal()
|
||||
|
||||
const width = "200px"
|
||||
const height = "150px"
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", p: theme.spacing(1, 0) }}>
|
||||
{study.artifacts.map((artifact) => {
|
||||
const urlPath = `/artifacts/${study.id}/${artifact.artifact_id}`
|
||||
return (
|
||||
<Card
|
||||
key={artifact.artifact_id}
|
||||
sx={{
|
||||
marginBottom: theme.spacing(2),
|
||||
width: width,
|
||||
margin: theme.spacing(0, 1, 1, 0),
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<ArtifactCardMedia
|
||||
artifact={artifact}
|
||||
urlPath={urlPath}
|
||||
height={height}
|
||||
/>
|
||||
<CardContent
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
padding: `${theme.spacing(1)} !important`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
p: theme.spacing(0.5, 0),
|
||||
flexGrow: 1,
|
||||
wordWrap: "break-word",
|
||||
maxWidth: `calc(100% - ${
|
||||
isThreejsArtifact(artifact)
|
||||
? theme.spacing(12)
|
||||
: theme.spacing(8)
|
||||
})`,
|
||||
}}
|
||||
>
|
||||
{artifact.filename}
|
||||
</Typography>
|
||||
{isThreejsArtifact(artifact) ? (
|
||||
<IconButton
|
||||
aria-label="show artifact 3d model"
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ margin: "auto 0" }}
|
||||
onClick={() => {
|
||||
openThreejsArtifactModal(urlPath, artifact)
|
||||
}}
|
||||
>
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
<IconButton
|
||||
aria-label="delete artifact"
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ margin: "auto 0" }}
|
||||
onClick={() => {
|
||||
openDeleteArtifactDialog(study.id, artifact)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="download artifact"
|
||||
size="small"
|
||||
color="inherit"
|
||||
download={artifact.filename}
|
||||
sx={{ margin: "auto 0" }}
|
||||
href={urlPath}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
<StudyArtifactUploader study={study} width={width} height={height} />
|
||||
</Box>
|
||||
{renderDeleteArtifactDialog()}
|
||||
{renderThreejsArtifactModal()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const StudyArtifactUploader: FC<{
|
||||
study: StudyDetail
|
||||
width: string
|
||||
height: string
|
||||
}> = ({ study, width, height }) => {
|
||||
const theme = useTheme()
|
||||
const [dragOver, setDragOver] = useState<boolean>(false)
|
||||
const action = actionCreator()
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const handleClick: MouseEventHandler = () => {
|
||||
if (!inputRef || !inputRef.current) {
|
||||
return
|
||||
}
|
||||
inputRef.current.click()
|
||||
}
|
||||
|
||||
const handleOnChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
const files = e.target.files
|
||||
if (files === null) {
|
||||
return
|
||||
}
|
||||
action.uploadStudyArtifact(study.id, files[0])
|
||||
}
|
||||
|
||||
const handleDragOver: DragEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = "copy"
|
||||
setDragOver(true)
|
||||
}
|
||||
|
||||
const handleDragLeave: DragEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = "copy"
|
||||
setDragOver(false)
|
||||
}
|
||||
|
||||
const handleDrop: DragEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
const files = e.dataTransfer.files
|
||||
setDragOver(false)
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
action.uploadStudyArtifact(study.id, files[i])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
marginBottom: theme.spacing(2),
|
||||
width: width,
|
||||
minHeight: height,
|
||||
margin: theme.spacing(0, 1, 1, 0),
|
||||
border: dragOver
|
||||
? `3px dashed ${theme.palette.mode === "dark" ? "white" : "black"}`
|
||||
: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<CardActionArea
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<UploadFileIcon
|
||||
sx={{ fontSize: 80, marginBottom: theme.spacing(2) }}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
ref={inputRef}
|
||||
onChange={handleOnChange}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<Typography>Upload a New File</Typography>
|
||||
<Typography
|
||||
sx={{ textAlign: "center", color: theme.palette.grey.A400 }}
|
||||
>
|
||||
Drag your file here or click to browse.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import Grid2 from "@mui/material/Unstable_Grid2"
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight"
|
||||
import HomeIcon from "@mui/icons-material/Home"
|
||||
import DownloadIcon from "@mui/icons-material/Download"
|
||||
|
||||
import { StudyNote } from "./Note"
|
||||
import { actionCreator } from "../action"
|
||||
@@ -27,6 +28,7 @@ import { GraphParallelCoordinate } from "./GraphParallelCoordinate"
|
||||
import { Contour } from "./GraphContour"
|
||||
import { GraphSlice } from "./GraphSlice"
|
||||
import { GraphEdf } from "./GraphEdf"
|
||||
import { GraphRank } from "./GraphRank"
|
||||
import { TrialList } from "./TrialList"
|
||||
import { StudyHistory } from "./StudyHistory"
|
||||
import { PreferentialTrials } from "./PreferentialTrials"
|
||||
@@ -121,6 +123,11 @@ export const StudyDetail: FC<{
|
||||
<Contour study={studyDetail} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<GraphRank study={studyDetail} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Typography variant="h5" sx={{ margin: theme.spacing(2) }}>
|
||||
Empirical Distribution of the Objective Value
|
||||
</Typography>
|
||||
@@ -139,16 +146,44 @@ export const StudyDetail: FC<{
|
||||
</Grid2>
|
||||
</Box>
|
||||
)
|
||||
} else if (page === "trialTable") {
|
||||
content = (
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<TrialTable studyDetail={studyDetail} initialRowsPerPage={50} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
} else if (page === "trialList") {
|
||||
content = <TrialList studyDetail={studyDetail} />
|
||||
} else if (page === "trialTable") {
|
||||
content = (
|
||||
<Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}>
|
||||
<Card
|
||||
sx={{
|
||||
margin: theme.spacing(2),
|
||||
width: "auto",
|
||||
height: "auto",
|
||||
display: "flex",
|
||||
justifyContent: "left",
|
||||
alignItems: "left",
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<IconButton
|
||||
aria-label="download csv"
|
||||
size="small"
|
||||
color="inherit"
|
||||
download
|
||||
sx={{ margin: "auto 0" }}
|
||||
href={`/csv/${studyDetail?.id}`}
|
||||
>
|
||||
<DownloadIcon />
|
||||
<Typography variant="button" sx={{ margin: theme.spacing(2) }}>
|
||||
Download CSV File
|
||||
</Typography>
|
||||
</IconButton>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<TrialTable studyDetail={studyDetail} initialRowsPerPage={50} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
)
|
||||
} else if (page === "note" && studyDetail !== null) {
|
||||
content = (
|
||||
<Box
|
||||
@@ -186,7 +221,7 @@ export const StudyDetail: FC<{
|
||||
<PreferentialGraph studyDetail={studyDetail} />
|
||||
</Box>
|
||||
)
|
||||
} else if (page == "preferenceHistory") {
|
||||
} else if (page === "preferenceHistory") {
|
||||
content = <PreferenceHistory studyDetail={studyDetail} />
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,15 @@ import { DataGrid, DataGridColumn } from "./DataGrid"
|
||||
import { GraphHyperparameterImportance } from "./GraphHyperparameterImportances"
|
||||
import { UserDefinedPlot } from "./UserDefinedPlot"
|
||||
import { BestTrialsCard } from "./BestTrialsCard"
|
||||
import { StudyArtifactCards } from "./StudyArtifactCards"
|
||||
import { useRecoilValue } from "recoil"
|
||||
import {
|
||||
useStudyDetailValue,
|
||||
useStudyDirections,
|
||||
useStudySummaryValue,
|
||||
} from "../state"
|
||||
import FormControlLabel from "@mui/material/FormControlLabel"
|
||||
import { artifactIsAvailable } from "../state"
|
||||
|
||||
export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
const theme = useTheme()
|
||||
@@ -31,6 +34,7 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
const studyDetail = useStudyDetailValue(studyId)
|
||||
const [logScale, setLogScale] = useState<boolean>(false)
|
||||
const [includePruned, setIncludePruned] = useState<boolean>(true)
|
||||
const artifactEnabled = useRecoilValue<boolean>(artifactIsAvailable)
|
||||
|
||||
const handleLogScaleChange = () => {
|
||||
setLogScale(!logScale)
|
||||
@@ -104,17 +108,6 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Grid2 container spacing={2} sx={{ padding: theme.spacing(0, 2) }}>
|
||||
{studyDetail !== null &&
|
||||
studyDetail.directions.length == 1 &&
|
||||
studyDetail.has_intermediate_values ? (
|
||||
<Grid2 xs={6}>
|
||||
<GraphIntermediateValues
|
||||
trials={trials}
|
||||
includePruned={includePruned}
|
||||
logScale={logScale}
|
||||
/>
|
||||
</Grid2>
|
||||
) : null}
|
||||
<Grid2 xs={6}>
|
||||
<GraphHyperparameterImportance
|
||||
studyId={studyId}
|
||||
@@ -166,7 +159,44 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
{studyDetail !== null &&
|
||||
studyDetail.directions.length === 1 &&
|
||||
studyDetail.has_intermediate_values ? (
|
||||
<Grid2 xs={6}>
|
||||
<GraphIntermediateValues
|
||||
trials={trials}
|
||||
includePruned={includePruned}
|
||||
logScale={logScale}
|
||||
/>
|
||||
</Grid2>
|
||||
) : null}
|
||||
</Grid2>
|
||||
|
||||
{artifactEnabled && studyDetail !== null && (
|
||||
<Grid2 container spacing={2} sx={{ padding: theme.spacing(0, 2) }}>
|
||||
<Grid2 xs={6}>
|
||||
<Card>
|
||||
<CardContent
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
margin: "1em 0",
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
}}
|
||||
>
|
||||
Study Artifacts
|
||||
</Typography>
|
||||
<StudyArtifactCards study={studyDetail} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,8 +64,7 @@ export const StudyList: FC<{
|
||||
return useMemo(() => new URLSearchParams(search), [search])
|
||||
}
|
||||
const query = useQuery()
|
||||
const initialSortBy =
|
||||
query.get("studies_order_by") === "desc" ? "desc" : "asc"
|
||||
const initialSortBy = query.get("studies_order_by") === "asc" ? "asc" : "desc"
|
||||
const [sortBy, setSortBy] = useState<"asc" | "desc">(initialSortBy)
|
||||
|
||||
let filteredStudies = studies.filter((s) => !studyFilter(s))
|
||||
|
||||
@@ -4,12 +4,17 @@ import { Canvas } from "@react-three/fiber"
|
||||
import { GizmoHelper, GizmoViewport, OrbitControls } from "@react-three/drei"
|
||||
import { STLLoader } from "three/examples/jsm/loaders/STLLoader"
|
||||
import { Rhino3dmLoader } from "three/examples/jsm/loaders/3DMLoader"
|
||||
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader"
|
||||
import { PerspectiveCamera } from "three"
|
||||
import { Modal, Box } from "@mui/material"
|
||||
import { Modal, Box, useTheme } from "@mui/material"
|
||||
import ClearIcon from "@mui/icons-material/Clear"
|
||||
import IconButton from "@mui/material/IconButton"
|
||||
|
||||
export const isThreejsArtifact = (artifact: Artifact): boolean => {
|
||||
return (
|
||||
artifact.filename.endsWith(".stl") || artifact.filename.endsWith(".3dm")
|
||||
artifact.filename.endsWith(".stl") ||
|
||||
artifact.filename.endsWith(".3dm") ||
|
||||
artifact.filename.endsWith(".obj")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,7 +37,7 @@ const CustomGizmoHelper: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const calculateBoundingBox = (geometries: THREE.BufferGeometry[]) => {
|
||||
const computeBoundingBox = (geometries: THREE.BufferGeometry[]) => {
|
||||
const boundingBox = new THREE.Box3()
|
||||
geometries.forEach((geometry) => {
|
||||
const mesh = new THREE.Mesh(geometry)
|
||||
@@ -45,8 +50,11 @@ export const ThreejsArtifactViewer: React.FC<ThreejsArtifactViewerProps> = (
|
||||
props
|
||||
) => {
|
||||
const [geometry, setGeometry] = useState<THREE.BufferGeometry[]>([])
|
||||
const [modelSize, setModelSize] = useState<THREE.Vector3>(
|
||||
new THREE.Vector3(10, 10, 10)
|
||||
const [boundingBox, setBoundingBox] = useState<THREE.Box3>(
|
||||
new THREE.Box3(
|
||||
new THREE.Vector3(-10, -10, -10),
|
||||
new THREE.Vector3(10, 10, 10)
|
||||
)
|
||||
)
|
||||
const [cameraSettings, setCameraSettings] = useState<PerspectiveCamera>(
|
||||
new THREE.PerspectiveCamera()
|
||||
@@ -54,55 +62,54 @@ export const ThreejsArtifactViewer: React.FC<ThreejsArtifactViewerProps> = (
|
||||
|
||||
const handleLoadedGeometries = (geometries: THREE.BufferGeometry[]) => {
|
||||
setGeometry(geometries)
|
||||
const boundingBox = calculateBoundingBox(geometries)
|
||||
const boundingBox = computeBoundingBox(geometries)
|
||||
if (boundingBox !== null) {
|
||||
const size = boundingBox.getSize(new THREE.Vector3())
|
||||
setModelSize(size)
|
||||
setBoundingBox(boundingBox)
|
||||
}
|
||||
return boundingBox
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if ("stl" === props.filetype) {
|
||||
const stlLoader = new STLLoader()
|
||||
stlLoader.load(props.src, (stlGeometries: THREE.BufferGeometry) => {
|
||||
if (stlGeometries) {
|
||||
handleLoadedGeometries([stlGeometries])
|
||||
}
|
||||
})
|
||||
loadSTL(props, handleLoadedGeometries)
|
||||
} else if ("3dm" === props.filetype) {
|
||||
const loader = new Rhino3dmLoader()
|
||||
loader.setLibraryPath("https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/")
|
||||
loader.load(props.src, (object: THREE.Object3D) => {
|
||||
const meshes = object.children as THREE.Mesh[]
|
||||
const rhinoGeometries = meshes.map((mesh) => mesh.geometry)
|
||||
THREE.Object3D.DEFAULT_UP.set(0, 0, 1)
|
||||
if (rhinoGeometries.length > 0) {
|
||||
handleLoadedGeometries(rhinoGeometries)
|
||||
}
|
||||
})
|
||||
loadRhino3dm(props, handleLoadedGeometries)
|
||||
} else if ("obj" === props.filetype) {
|
||||
loadOBJ(props, handleLoadedGeometries)
|
||||
}
|
||||
const maxModelSize = Math.max(modelSize.x, modelSize.y, modelSize.z)
|
||||
const cameraSet = new THREE.PerspectiveCamera(
|
||||
modelSize
|
||||
? Math.min(
|
||||
45,
|
||||
Math.atan(modelSize.y / modelSize.z) * (180 / Math.PI) * 2
|
||||
)
|
||||
: 45,
|
||||
window.innerWidth / window.innerHeight
|
||||
)
|
||||
cameraSet.position.set(maxModelSize * 2, maxModelSize * 2, maxModelSize * 2)
|
||||
setCameraSettings(cameraSet)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const cameraSet = new THREE.PerspectiveCamera(
|
||||
50,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
boundingBox.getSize(new THREE.Vector3()).length() * 100
|
||||
)
|
||||
const maxPosition = Math.max(
|
||||
boundingBox.max.x,
|
||||
boundingBox.max.y,
|
||||
boundingBox.max.z
|
||||
)
|
||||
cameraSet.position.set(
|
||||
maxPosition * 1.5,
|
||||
maxPosition * 1.5,
|
||||
maxPosition * 1.5
|
||||
)
|
||||
const center = boundingBox.getCenter(new THREE.Vector3())
|
||||
cameraSet.lookAt(center.x, center.y, center.z)
|
||||
setCameraSettings(cameraSet)
|
||||
}, [boundingBox])
|
||||
|
||||
return (
|
||||
<Canvas
|
||||
frameloop="demand"
|
||||
camera={cameraSettings}
|
||||
style={{ width: props.width, height: props.height }}
|
||||
>
|
||||
<ambientLight />
|
||||
<OrbitControls />
|
||||
<gridHelper args={[Math.max(modelSize?.x, modelSize?.y) * 5]} />
|
||||
<gridHelper args={[Math.max(boundingBox.max.x, boundingBox.max.y) * 5]} />
|
||||
{props.hasGizmo && <CustomGizmoHelper />}
|
||||
<axesHelper />
|
||||
{geometry.length > 0 &&
|
||||
@@ -121,6 +128,7 @@ export const useThreejsArtifactModal = (): [
|
||||
] => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [target, setTarget] = useState<[string, Artifact | null]>(["", null])
|
||||
const theme = useTheme()
|
||||
|
||||
const openModal = (artifactUrlPath: string, artifact: Artifact) => {
|
||||
setTarget([artifactUrlPath, artifact])
|
||||
@@ -146,6 +154,19 @@ export const useThreejsArtifactModal = (): [
|
||||
borderRadius: "15px",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: theme.spacing(2),
|
||||
right: theme.spacing(2),
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setTarget(["", null])
|
||||
}}
|
||||
>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
<ThreejsArtifactViewer
|
||||
src={target[0]}
|
||||
width={`${innerWidth * 0.8}px`}
|
||||
@@ -159,3 +180,45 @@ export const useThreejsArtifactModal = (): [
|
||||
}
|
||||
return [openModal, renderDeleteStudyDialog]
|
||||
}
|
||||
|
||||
function loadSTL(
|
||||
props: ThreejsArtifactViewerProps,
|
||||
handleLoadedGeometries: (geometries: THREE.BufferGeometry[]) => THREE.Box3
|
||||
) {
|
||||
const stlLoader = new STLLoader()
|
||||
stlLoader.load(props.src, (stlGeometries: THREE.BufferGeometry) => {
|
||||
if (stlGeometries) {
|
||||
handleLoadedGeometries([stlGeometries])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function loadRhino3dm(
|
||||
props: ThreejsArtifactViewerProps,
|
||||
handleLoadedGeometries: (geometries: THREE.BufferGeometry[]) => THREE.Box3
|
||||
) {
|
||||
const rhino3dmLoader = new Rhino3dmLoader()
|
||||
rhino3dmLoader.setLibraryPath("https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/")
|
||||
rhino3dmLoader.load(props.src, (object: THREE.Object3D) => {
|
||||
const meshes = object.children as THREE.Mesh[]
|
||||
const rhinoGeometries = meshes.map((mesh) => mesh.geometry)
|
||||
THREE.Object3D.DEFAULT_UP.set(0, 0, 1)
|
||||
if (rhinoGeometries.length > 0) {
|
||||
handleLoadedGeometries(rhinoGeometries)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function loadOBJ(
|
||||
props: ThreejsArtifactViewerProps,
|
||||
handleLoadedGeometries: (geometries: THREE.BufferGeometry[]) => THREE.Box3
|
||||
) {
|
||||
const objLoader = new OBJLoader()
|
||||
objLoader.load(props.src, (object: THREE.Object3D) => {
|
||||
const meshes = object.children as THREE.Mesh[]
|
||||
const objGeometries = meshes.map((mesh) => mesh.geometry)
|
||||
if (objGeometries.length > 0) {
|
||||
handleLoadedGeometries(objGeometries)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import DeleteIcon from "@mui/icons-material/Delete"
|
||||
import FullscreenIcon from "@mui/icons-material/Fullscreen"
|
||||
|
||||
import { actionCreator } from "../action"
|
||||
import { useDeleteArtifactDialog } from "./DeleteArtifactDialog"
|
||||
import { useDeleteTrialArtifactDialog } from "./DeleteArtifactDialog"
|
||||
import {
|
||||
useThreejsArtifactModal,
|
||||
isThreejsArtifact,
|
||||
@@ -31,9 +31,12 @@ import { ArtifactCardMedia } from "./ArtifactCardMedia"
|
||||
export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => {
|
||||
const theme = useTheme()
|
||||
const [openDeleteArtifactDialog, renderDeleteArtifactDialog] =
|
||||
useDeleteArtifactDialog()
|
||||
useDeleteTrialArtifactDialog()
|
||||
const [openThreejsArtifactModal, renderThreejsArtifactModal] =
|
||||
useThreejsArtifactModal()
|
||||
const isArtifactModifiable = (trial: Trial) => {
|
||||
return trial.state === "Running" || trial.state === "Waiting"
|
||||
}
|
||||
|
||||
const width = "200px"
|
||||
const height = "150px"
|
||||
@@ -75,11 +78,11 @@ export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => {
|
||||
p: theme.spacing(0.5, 0),
|
||||
flexGrow: 1,
|
||||
wordWrap: "break-word",
|
||||
maxWidth: `calc(100% - ${
|
||||
isThreejsArtifact(artifact)
|
||||
? theme.spacing(12)
|
||||
: theme.spacing(8)
|
||||
})`,
|
||||
maxWidth: `calc(100% - ${theme.spacing(
|
||||
4 +
|
||||
(isThreejsArtifact(artifact) ? 4 : 0) +
|
||||
(isArtifactModifiable(trial) ? 4 : 0)
|
||||
)})`,
|
||||
}}
|
||||
>
|
||||
{artifact.filename}
|
||||
@@ -97,21 +100,23 @@ export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => {
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
<IconButton
|
||||
aria-label="delete artifact"
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ margin: "auto 0" }}
|
||||
onClick={() => {
|
||||
openDeleteArtifactDialog(
|
||||
trial.study_id,
|
||||
trial.trial_id,
|
||||
artifact
|
||||
)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
{isArtifactModifiable(trial) ? (
|
||||
<IconButton
|
||||
aria-label="delete artifact"
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ margin: "auto 0" }}
|
||||
onClick={() => {
|
||||
openDeleteArtifactDialog(
|
||||
trial.study_id,
|
||||
trial.trial_id,
|
||||
artifact
|
||||
)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
<IconButton
|
||||
aria-label="download artifact"
|
||||
size="small"
|
||||
@@ -126,7 +131,9 @@ export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => {
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
<TrialArtifactUploader trial={trial} width={width} height={height} />
|
||||
{isArtifactModifiable(trial) ? (
|
||||
<TrialArtifactUploader trial={trial} width={width} height={height} />
|
||||
) : null}
|
||||
</Box>
|
||||
{renderDeleteArtifactDialog()}
|
||||
{renderThreejsArtifactModal()}
|
||||
@@ -143,9 +150,6 @@ const TrialArtifactUploader: FC<{
|
||||
const action = actionCreator()
|
||||
const [dragOver, setDragOver] = useState<boolean>(false)
|
||||
|
||||
if (trial.state !== "Running" && trial.state !== "Waiting") {
|
||||
return null
|
||||
}
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const handleClick: MouseEventHandler = () => {
|
||||
if (!inputRef || !inputRef.current) {
|
||||
@@ -158,7 +162,7 @@ const TrialArtifactUploader: FC<{
|
||||
if (files === null) {
|
||||
return
|
||||
}
|
||||
action.uploadArtifact(trial.study_id, trial.trial_id, files[0])
|
||||
action.uploadTrialArtifact(trial.study_id, trial.trial_id, files[0])
|
||||
}
|
||||
const handleDrop: DragEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
@@ -166,7 +170,7 @@ const TrialArtifactUploader: FC<{
|
||||
const files = e.dataTransfer.files
|
||||
setDragOver(false)
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
action.uploadArtifact(trial.study_id, trial.trial_id, files[i])
|
||||
action.uploadTrialArtifact(trial.study_id, trial.trial_id, files[i])
|
||||
}
|
||||
}
|
||||
const handleDragOver: DragEventHandler = (e) => {
|
||||
|
||||
@@ -45,12 +45,12 @@ export const TrialFormWidgets: FC<{
|
||||
? "Set Objective Values Form"
|
||||
: "Set Objective Value Form"
|
||||
const widgetNames = formWidgets.widgets.map((widget, i) => {
|
||||
if (formWidgets.output_type == "objective") {
|
||||
if (formWidgets.output_type === "objective") {
|
||||
if (objectiveNames.at(i) !== undefined) {
|
||||
return objectiveNames[i]
|
||||
}
|
||||
return directions.length == 1 ? "Objective" : `Objective ${i}`
|
||||
} else if (formWidgets.output_type == "user_attr") {
|
||||
return directions.length === 1 ? "Objective" : `Objective ${i}`
|
||||
} else if (formWidgets.output_type === "user_attr") {
|
||||
if (widget.type !== "user_attr" && widget.user_attr_key !== undefined) {
|
||||
return widget.user_attr_key
|
||||
}
|
||||
@@ -118,13 +118,13 @@ const UpdatableFormWidgets: FC<{
|
||||
const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
e.preventDefault()
|
||||
const values = widgetStates.map((ws) => ws.value)
|
||||
if (formWidgets.output_type == "objective") {
|
||||
if (formWidgets.output_type === "objective") {
|
||||
const filtered = values.filter<number>((v): v is number => v !== null)
|
||||
if (filtered.length !== formWidgets.widgets.length) {
|
||||
return
|
||||
}
|
||||
action.makeTrialComplete(trial.study_id, trial.trial_id, filtered)
|
||||
} else if (formWidgets.output_type == "user_attr") {
|
||||
} else if (formWidgets.output_type === "user_attr") {
|
||||
const user_attrs = Object.fromEntries(
|
||||
formWidgets.widgets.map((widget, i) => [
|
||||
widget.user_attr_key,
|
||||
@@ -433,7 +433,7 @@ const ReadonlyFormWidgets: FC<{
|
||||
max={widget.max}
|
||||
step={widget.step}
|
||||
marks={
|
||||
widget.labels === null || widget.labels.length == 0
|
||||
widget.labels === null || widget.labels.length === 0
|
||||
? true
|
||||
: widget.labels
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ const useIsBestTrial = (
|
||||
return useMemo(() => {
|
||||
const bestTrialIDs = studyDetail?.best_trials.map((t) => t.trial_id) || []
|
||||
return (trialId: number): boolean =>
|
||||
bestTrialIDs.findIndex((a) => a === trialId) != -1
|
||||
bestTrialIDs.findIndex((a) => a === trialId) !== -1
|
||||
}, [studyDetail])
|
||||
}
|
||||
|
||||
|
||||
@@ -18,17 +18,17 @@ export const TrialTable: FC<{
|
||||
field: "state",
|
||||
label: "State",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterChoices: ["Complete", "Pruned", "Fail", "Running", "Waiting"],
|
||||
padding: "none",
|
||||
toCellValue: (i) => trials[i].state.toString(),
|
||||
},
|
||||
]
|
||||
if (studyDetail === null || studyDetail.directions.length == 1) {
|
||||
if (studyDetail === null || studyDetail.directions.length === 1) {
|
||||
columns.push({
|
||||
field: "values",
|
||||
label: "Value",
|
||||
sortable: true,
|
||||
less: (firstEl, secondEl): number => {
|
||||
less: (firstEl, secondEl, ascending): number => {
|
||||
const firstVal = firstEl.values?.[0]
|
||||
const secondVal = secondEl.values?.[0]
|
||||
|
||||
@@ -36,9 +36,9 @@ export const TrialTable: FC<{
|
||||
return 0
|
||||
}
|
||||
if (firstVal === undefined) {
|
||||
return -1
|
||||
return ascending ? -1 : 1
|
||||
} else if (secondVal === undefined) {
|
||||
return 1
|
||||
return ascending ? 1 : -1
|
||||
}
|
||||
if (firstVal === "-inf" || secondVal === "inf") {
|
||||
return 1
|
||||
@@ -63,7 +63,7 @@ export const TrialTable: FC<{
|
||||
? objectiveNames[objectiveId]
|
||||
: `Objective ${objectiveId}`,
|
||||
sortable: true,
|
||||
less: (firstEl, secondEl): number => {
|
||||
less: (firstEl, secondEl, ascending): number => {
|
||||
const firstVal = firstEl.values?.[objectiveId]
|
||||
const secondVal = secondEl.values?.[objectiveId]
|
||||
|
||||
@@ -71,9 +71,9 @@ export const TrialTable: FC<{
|
||||
return 0
|
||||
}
|
||||
if (firstVal === undefined) {
|
||||
return -1
|
||||
return ascending ? -1 : 1
|
||||
} else if (secondVal === undefined) {
|
||||
return 1
|
||||
return ascending ? 1 : -1
|
||||
}
|
||||
if (firstVal === "-inf" || secondVal === "inf") {
|
||||
return 1
|
||||
@@ -97,7 +97,10 @@ export const TrialTable: FC<{
|
||||
) {
|
||||
studyDetail?.intersection_search_space.forEach((s) => {
|
||||
const sortable = s.distribution.type !== "CategoricalDistribution"
|
||||
const filterable = 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}`,
|
||||
@@ -105,8 +108,9 @@ export const TrialTable: FC<{
|
||||
trials[i].params.find((p) => p.name === s.name)
|
||||
?.param_external_value || null,
|
||||
sortable: sortable,
|
||||
filterable: filterable,
|
||||
less: (firstEl, secondEl): number => {
|
||||
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
|
||||
@@ -145,8 +149,8 @@ export const TrialTable: FC<{
|
||||
trials[i].user_attrs.find((attr) => attr.key === attr_spec.key)
|
||||
?.value || null,
|
||||
sortable: attr_spec.sortable,
|
||||
filterable: !attr_spec.sortable,
|
||||
less: (firstEl, secondEl): number => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
less: (firstEl, secondEl, _): number => {
|
||||
const firstVal = firstEl.user_attrs.find(
|
||||
(attr) => attr.key === attr_spec.key
|
||||
)?.value
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { useCallback, useEffect, useState, useRef } from "react"
|
||||
import WaveSurfer from "wavesurfer.js"
|
||||
import { Box } from "@mui/material"
|
||||
|
||||
interface WaveSurferArtifactViewerProps {
|
||||
height: number
|
||||
waveColor: string
|
||||
progressColor: string
|
||||
url: string
|
||||
}
|
||||
|
||||
const useWavesurfer = (
|
||||
containerRef: React.MutableRefObject<HTMLDivElement>,
|
||||
options: WaveSurferArtifactViewerProps
|
||||
) => {
|
||||
const [wavesurfer, setWavesurfer] = useState<WaveSurfer | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return
|
||||
|
||||
const ws = WaveSurfer.create({
|
||||
...options,
|
||||
container: containerRef.current,
|
||||
})
|
||||
|
||||
setWavesurfer(ws)
|
||||
|
||||
return () => {
|
||||
ws.destroy()
|
||||
}
|
||||
}, [containerRef])
|
||||
|
||||
return wavesurfer
|
||||
}
|
||||
|
||||
// Create a React component of wavesurfer.
|
||||
export const WaveSurferArtifactViewer: React.FC<
|
||||
WaveSurferArtifactViewerProps
|
||||
> = (props) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null!)
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const wavesurfer = useWavesurfer(containerRef, props)
|
||||
|
||||
const onPlayClick = useCallback(() => {
|
||||
if (!wavesurfer) return
|
||||
wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play()
|
||||
}, [wavesurfer])
|
||||
|
||||
useEffect(() => {
|
||||
if (!wavesurfer) return
|
||||
|
||||
setIsPlaying(false)
|
||||
|
||||
const subscriptions = [
|
||||
wavesurfer.on("play", () => setIsPlaying(true)),
|
||||
wavesurfer.on("pause", () => setIsPlaying(false)),
|
||||
]
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach((unsub) => unsub())
|
||||
}
|
||||
}, [wavesurfer])
|
||||
|
||||
return (
|
||||
<Box style={{ width: "100%", display: "flex", flexDirection: "column" }}>
|
||||
<div ref={containerRef} style={{ minHeight: "120px", width: "100%" }} />
|
||||
<button onClick={onPlayClick} style={{ marginTop: "1em" }}>
|
||||
{isPlaying ? "Pause" : "Play"}
|
||||
</button>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export const getDominatedTrials = (
|
||||
const dominatedTrials: boolean[] = []
|
||||
normalizedValues.forEach((values0: number[], i: number) => {
|
||||
const dominated = normalizedValues.some((values1: number[], j: number) => {
|
||||
if (i === j || values0.every((v, i) => v == values1[i])) {
|
||||
if (i === j || values0.every((v, i) => v === values1[i])) {
|
||||
return false
|
||||
}
|
||||
return values0.every((value0: number, k: number) => {
|
||||
|
||||
@@ -1,3 +1,104 @@
|
||||
const PADDING_RATIO = 0.05
|
||||
|
||||
export type AxisInfo = {
|
||||
name: string
|
||||
isLog: boolean
|
||||
isCat: boolean
|
||||
indices: (string | number)[]
|
||||
values: (string | number | null)[]
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const unique = (array: any[]) => {
|
||||
const knownElements = new Map()
|
||||
array.forEach((elem) => knownElements.set(elem, true))
|
||||
return Array.from(knownElements.keys())
|
||||
}
|
||||
|
||||
export const getAxisInfo = (
|
||||
trials: Trial[],
|
||||
param: SearchSpaceItem
|
||||
): AxisInfo => {
|
||||
if (param.distribution.type === "CategoricalDistribution") {
|
||||
return getAxisInfoForCategoricalParams(
|
||||
trials,
|
||||
param.name,
|
||||
param.distribution
|
||||
)
|
||||
} else {
|
||||
return getAxisInfoForNumericalParams(trials, param.name, param.distribution)
|
||||
}
|
||||
}
|
||||
|
||||
const getAxisInfoForCategoricalParams = (
|
||||
trials: Trial[],
|
||||
paramName: string,
|
||||
distribution: CategoricalDistribution
|
||||
): AxisInfo => {
|
||||
const values = trials.map(
|
||||
(trial) =>
|
||||
trial.params.find((p) => p.name === paramName)?.param_external_value ||
|
||||
null
|
||||
)
|
||||
|
||||
const indices = distribution.choices
|
||||
.map((c) => c.value)
|
||||
.sort((a, b) =>
|
||||
a.toLowerCase() < b.toLowerCase()
|
||||
? -1
|
||||
: a.toLowerCase() > b.toLowerCase()
|
||||
? 1
|
||||
: 0
|
||||
)
|
||||
return {
|
||||
name: paramName,
|
||||
isLog: false,
|
||||
isCat: true,
|
||||
indices,
|
||||
values,
|
||||
}
|
||||
}
|
||||
|
||||
const getAxisInfoForNumericalParams = (
|
||||
trials: Trial[],
|
||||
paramName: string,
|
||||
distribution: FloatDistribution | IntDistribution
|
||||
): AxisInfo => {
|
||||
let min = 0
|
||||
let max = 0
|
||||
if (distribution.log) {
|
||||
const padding =
|
||||
(Math.log10(distribution.high) - Math.log10(distribution.low)) *
|
||||
PADDING_RATIO
|
||||
min = Math.pow(10, Math.log10(distribution.low) - padding)
|
||||
max = Math.pow(10, Math.log10(distribution.high) + padding)
|
||||
} else {
|
||||
const padding = (distribution.high - distribution.low) * PADDING_RATIO
|
||||
min = distribution.low - padding
|
||||
max = distribution.high + padding
|
||||
}
|
||||
|
||||
const values = trials.map(
|
||||
(trial) =>
|
||||
trial.params.find((p) => p.name === paramName)?.param_internal_value ||
|
||||
null
|
||||
)
|
||||
const indices = unique(values)
|
||||
.filter((v) => v !== null)
|
||||
.sort((a, b) => a - b)
|
||||
if (indices.length >= 2) {
|
||||
indices.unshift(min)
|
||||
indices.push(max)
|
||||
}
|
||||
return {
|
||||
name: paramName,
|
||||
isLog: distribution.log,
|
||||
isCat: false,
|
||||
indices,
|
||||
values,
|
||||
}
|
||||
}
|
||||
|
||||
export const makeHovertext = (trial: Trial): string => {
|
||||
return JSON.stringify(
|
||||
{
|
||||
|
||||
@@ -63,7 +63,7 @@ export const useStudyDetailValue = (studyId: number): StudyDetail | null => {
|
||||
|
||||
export const useStudySummaryValue = (studyId: number): StudySummary | null => {
|
||||
const studySummaries = useRecoilValue<StudySummary[]>(studySummariesState)
|
||||
return studySummaries.find((s) => s.study_id == studyId) || null
|
||||
return studySummaries.find((s) => s.study_id === studyId) || null
|
||||
}
|
||||
|
||||
export const useTrialUpdatingValue = (trialId: number): boolean => {
|
||||
|
||||
Generated
+961
-899
File diff suppressed because it is too large
Load Diff
+6
-5
@@ -26,7 +26,7 @@
|
||||
"@react-three/drei": "^9.80.0",
|
||||
"@react-three/fiber": "^8.13.6",
|
||||
"@types/three": "^0.154.0",
|
||||
"axios": "^1.2.1",
|
||||
"axios": "^1.6.0",
|
||||
"elkjs": "^0.8.2",
|
||||
"notistack": "^3.0.1",
|
||||
"plotly.js-dist-min": "^2.22.0",
|
||||
@@ -41,7 +41,8 @@
|
||||
"rehype-raw": "^6.1.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"three": "^0.155.0"
|
||||
"three": "^0.155.0",
|
||||
"wavesurfer.js": "^7.4.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.3",
|
||||
@@ -52,12 +53,12 @@
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react-syntax-highlighter": "^15.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.1",
|
||||
"@typescript-eslint/parser": "^4.26.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"esbuild-loader": "^2.18.0",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint": "^8.53.0",
|
||||
"jest": "^29.2.1",
|
||||
"jest-canvas-mock": "^2.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
|
||||
+5
-1
@@ -36,6 +36,7 @@ dynamic = ["version"]
|
||||
[project.optional-dependencies]
|
||||
docs = [
|
||||
"boto3",
|
||||
"botorch",
|
||||
"streamlit",
|
||||
"sphinx",
|
||||
"sphinx_rtd_theme",
|
||||
@@ -51,9 +52,12 @@ test = [
|
||||
optional = [
|
||||
"streamlit",
|
||||
"boto3",
|
||||
"botorch",
|
||||
"botorch>=0.8.1; python_version>='3.8'",
|
||||
]
|
||||
|
||||
preferential = [
|
||||
"botorch>=0.8.1",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
optuna-dashboard = "optuna_dashboard._cli:main"
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import base64
|
||||
import json
|
||||
import tempfile
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import optuna
|
||||
from optuna.artifacts import FileSystemArtifactStore
|
||||
from optuna.artifacts import upload_artifact
|
||||
from optuna.storages import BaseStorage
|
||||
from optuna_dashboard._app import create_app
|
||||
from optuna_dashboard.artifact import _backend
|
||||
import pytest
|
||||
|
||||
from ..wsgi_client import send_request
|
||||
|
||||
|
||||
def test_get_artifact_path() -> None:
|
||||
study = MagicMock(_study_id=0)
|
||||
@@ -12,7 +21,7 @@ def test_get_artifact_path() -> None:
|
||||
|
||||
|
||||
def test_artifact_prefix() -> None:
|
||||
actual = _backend._dashboard_trial_artifact_prefix(trial_id=0)
|
||||
actual = _backend._dashboard_artifact_prefix(trial_id=0)
|
||||
assert actual == "dashboard:artifacts:0:"
|
||||
|
||||
|
||||
@@ -80,3 +89,163 @@ def test_list_trial_artifacts(init_storage_with_artifact_meta: MagicMock) -> Non
|
||||
{"artifact_id": "id1", "filename": "bar.txt"},
|
||||
{"artifact_id": "id2", "filename": "baz.txt"},
|
||||
]
|
||||
|
||||
|
||||
def test_study_artifact_store_none() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
app = create_app(storage)
|
||||
status, _, body = send_request(
|
||||
app,
|
||||
"/artifacts/0/0",
|
||||
"GET",
|
||||
)
|
||||
assert status == 400
|
||||
|
||||
|
||||
def test_study_artifact_not_found() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
artifact_store = FileSystemArtifactStore(tmpdir)
|
||||
app = create_app(storage, artifact_store)
|
||||
status, _, body = send_request(
|
||||
app,
|
||||
f"/artifacts/{study._study_id}/abc123",
|
||||
"GET",
|
||||
)
|
||||
assert status == 404
|
||||
|
||||
|
||||
def test_successful_study_artifact_retrieval() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
artifact_store = FileSystemArtifactStore(tmpdir)
|
||||
with tempfile.NamedTemporaryFile() as f:
|
||||
f.write(b"dummy_content")
|
||||
f.flush()
|
||||
artifact_id = upload_artifact(study, f.name, artifact_store=artifact_store)
|
||||
app = create_app(storage, artifact_store)
|
||||
status, _, body = send_request(
|
||||
app,
|
||||
f"/artifacts/{study._study_id}/{artifact_id}",
|
||||
"GET",
|
||||
)
|
||||
assert status == 200
|
||||
assert body == b"dummy_content"
|
||||
|
||||
|
||||
def test_trial_artifact_store_none() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
app = create_app(storage)
|
||||
status, _, body = send_request(
|
||||
app,
|
||||
"/artifacts/0/0/0",
|
||||
"GET",
|
||||
)
|
||||
assert status == 400
|
||||
|
||||
|
||||
def test_trial_artifact_not_found() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
trial = study.ask()
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
artifact_store = FileSystemArtifactStore(tmpdir)
|
||||
app = create_app(storage, artifact_store)
|
||||
status, _, body = send_request(
|
||||
app,
|
||||
f"/artifacts/{study._study_id}/{trial._trial_id}/abc123",
|
||||
"GET",
|
||||
)
|
||||
assert status == 404
|
||||
|
||||
|
||||
def test_successful_trial_artifact_retrieval() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
trial = study.ask()
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
artifact_store = FileSystemArtifactStore(tmpdir)
|
||||
with tempfile.NamedTemporaryFile() as f:
|
||||
f.write(b"dummy_content")
|
||||
f.flush()
|
||||
artifact_id = upload_artifact(trial, f.name, artifact_store=artifact_store)
|
||||
app = create_app(storage, artifact_store)
|
||||
status, _, body = send_request(
|
||||
app,
|
||||
f"/artifacts/{study._study_id}/{trial._trial_id}/{artifact_id}",
|
||||
"GET",
|
||||
)
|
||||
assert status == 200
|
||||
assert body == b"dummy_content"
|
||||
|
||||
|
||||
DUMMY_DATA_URL = (
|
||||
f"data:text/plain; charset=utf-8,{base64.b64encode(b'dummy_content').decode('utf-8')}"
|
||||
)
|
||||
|
||||
|
||||
def test_upload_artifact_invalid_no_trial() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
artifact_store = FileSystemArtifactStore(tmpdir)
|
||||
|
||||
app = create_app(storage, artifact_store)
|
||||
study = optuna.create_study(storage=storage)
|
||||
|
||||
status, _, body = send_request(
|
||||
app,
|
||||
f"/api/artifacts/{study._study_id}/0",
|
||||
"POST",
|
||||
body=json.dumps({"file": DUMMY_DATA_URL}),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert status == 500 # TODO(contramundum53): This should return 400
|
||||
|
||||
|
||||
def test_upload_artifact_invalid_complete_trial() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
artifact_store = FileSystemArtifactStore(tmpdir)
|
||||
|
||||
app = create_app(storage, artifact_store)
|
||||
study = optuna.create_study(storage=storage)
|
||||
|
||||
study.add_trial(optuna.create_trial(value=1.0, distributions={}, params={}))
|
||||
trial = study.trials[-1]
|
||||
status, _, body = send_request(
|
||||
app,
|
||||
f"/api/artifacts/{study._study_id}/{trial._trial_id}",
|
||||
"POST",
|
||||
body=json.dumps({"file": DUMMY_DATA_URL}),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert status == 400
|
||||
|
||||
|
||||
def test_upload_artifact() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
|
||||
study = optuna.create_study(storage=storage)
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
artifact_store = FileSystemArtifactStore(tmpdir)
|
||||
|
||||
app = create_app(storage, artifact_store)
|
||||
|
||||
study.add_trial(optuna.create_trial(state=optuna.trial.TrialState.RUNNING))
|
||||
trial = study.trials[-1]
|
||||
status, _, body = send_request(
|
||||
app,
|
||||
f"/api/artifacts/{study._study_id}/{trial._trial_id}",
|
||||
"POST",
|
||||
body=json.dumps({"file": DUMMY_DATA_URL}),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert status == 201
|
||||
res = json.loads(body)
|
||||
with open(f"{tmpdir}/{res['artifact_id']}", "r") as f:
|
||||
data = f.read()
|
||||
assert data == "dummy_content"
|
||||
|
||||
@@ -3,12 +3,12 @@ from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from optuna_dashboard.preferential.samplers.gp import _one_side_trunc_norm_sampling
|
||||
from optuna_dashboard.preferential.samplers.gp import _orthants_MVN_Gibbs_sampling
|
||||
import torch
|
||||
else:
|
||||
pytest.skip("BoTorch dropped Python3.7 support", allow_module_level=True)
|
||||
|
||||
@@ -36,3 +36,19 @@ def test_one_side_trunc_norm_sampling() -> None:
|
||||
assert np.allclose(
|
||||
_one_side_trunc_norm_sampling(torch.Tensor([5])).numpy(), 5.426934003050024
|
||||
)
|
||||
|
||||
def test_one_side_trunc_norm_sampling() -> None:
|
||||
for lower in np.linspace(-10, 10, 100):
|
||||
assert _one_side_trunc_norm_sampling(torch.tensor([lower], dtype=torch.float64)) >= lower
|
||||
|
||||
with patch.object(torch, "rand", return_value=torch.tensor([0.4], dtype=torch.float64)):
|
||||
sampled_value = _one_side_trunc_norm_sampling(torch.tensor([0.1], dtype=torch.float64))
|
||||
assert np.allclose(sampled_value.numpy(), 0.899967154837563)
|
||||
|
||||
with patch.object(torch, "rand", return_value=torch.tensor([0.8], dtype=torch.float64)):
|
||||
sampled_value = _one_side_trunc_norm_sampling(torch.tensor([-2.3], dtype=torch.float64))
|
||||
assert np.allclose(sampled_value.numpy(), -0.8113606739551955)
|
||||
|
||||
with patch.object(torch, "rand", return_value=torch.tensor([0.1], dtype=torch.float64)):
|
||||
sampled_value = _one_side_trunc_norm_sampling(torch.tensor([5], dtype=torch.float64))
|
||||
assert np.allclose(sampled_value.numpy(), 5.426934003050024)
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import copy
|
||||
import multiprocessing
|
||||
import pickle
|
||||
import sys
|
||||
from typing import Callable
|
||||
from unittest.mock import patch
|
||||
import uuid
|
||||
@@ -22,6 +23,10 @@ from ..storage_supplier import parametrize_storages
|
||||
from ..storage_supplier import StorageSupplier
|
||||
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
pytest.skip("BoTorch dropped Python3.7 support", allow_module_level=True)
|
||||
|
||||
|
||||
@parametrize_storages
|
||||
def test_study_set_and_get_user_attrs(storage_supplier: Callable[[], StorageSupplier]) -> None:
|
||||
with storage_supplier() as storage:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from unittest import TestCase
|
||||
|
||||
import optuna
|
||||
@@ -8,12 +9,15 @@ from optuna import get_all_study_summaries
|
||||
from optuna.study import StudyDirection
|
||||
from optuna_dashboard._app import create_app
|
||||
from optuna_dashboard._app import create_new_study
|
||||
from optuna_dashboard._note import note_str_key_prefix
|
||||
from optuna_dashboard._note import note_ver_key
|
||||
from optuna_dashboard._preference_setting import register_preference_feedback_component
|
||||
from optuna_dashboard._preferential_history import NewHistory
|
||||
from optuna_dashboard._preferential_history import remove_history
|
||||
from optuna_dashboard._preferential_history import report_history
|
||||
from optuna_dashboard._serializer import serialize_preference_history
|
||||
from optuna_dashboard.preferential import create_study
|
||||
import pytest
|
||||
|
||||
from .wsgi_client import send_request
|
||||
|
||||
@@ -105,6 +109,7 @@ class APITestCase(TestCase):
|
||||
)
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
|
||||
def test_get_best_trials_of_preferential_study(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = create_study(n_generate=4, storage=storage)
|
||||
@@ -128,6 +133,7 @@ class APITestCase(TestCase):
|
||||
assert len(best_trials) == 1
|
||||
assert best_trials[0]["number"] == 0
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
|
||||
def test_report_preference(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = create_study(n_generate=4, storage=storage)
|
||||
@@ -161,6 +167,7 @@ class APITestCase(TestCase):
|
||||
assert better.number == 2
|
||||
assert worse.number == 1
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
|
||||
def test_report_preference_when_typo_mode(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = create_study(storage=storage, n_generate=3)
|
||||
@@ -184,6 +191,7 @@ class APITestCase(TestCase):
|
||||
)
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
|
||||
def test_change_component(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = create_study(storage=storage, n_generate=3)
|
||||
@@ -214,6 +222,111 @@ class APITestCase(TestCase):
|
||||
assert study_detail["feedback_component_type"]["output_type"] == "artifact"
|
||||
assert study_detail["feedback_component_type"]["artifact_key"] == "image"
|
||||
|
||||
def test_save_trial_user_attrs(self) -> None:
|
||||
study = optuna.create_study()
|
||||
trials: list[optuna.Trial] = []
|
||||
for _ in range(2):
|
||||
trial = study.ask()
|
||||
trials.append(trial)
|
||||
|
||||
request_body = {
|
||||
"user_attrs": {
|
||||
"number": 0,
|
||||
},
|
||||
}
|
||||
|
||||
app = create_app(study._storage)
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trials[0]._trial_id}/user-attrs",
|
||||
"POST",
|
||||
content_type="application/json",
|
||||
body=json.dumps(request_body),
|
||||
)
|
||||
self.assertEqual(status, 204)
|
||||
|
||||
assert study.trials[0].user_attrs == request_body["user_attrs"]
|
||||
assert study.trials[1].user_attrs == {}
|
||||
|
||||
def test_save_trial_user_attrs_empty(self) -> None:
|
||||
study = optuna.create_study()
|
||||
trial = study.ask()
|
||||
|
||||
app = create_app(study._storage)
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial._trial_id}/user-attrs",
|
||||
"POST",
|
||||
content_type="application/json",
|
||||
body=json.dumps({}),
|
||||
)
|
||||
self.assertEqual(status, 400)
|
||||
assert study.trials[0].user_attrs == {}
|
||||
|
||||
def _save_trial_note(self, request_body: dict[str, int | str]) -> tuple[int, optuna.Study]:
|
||||
study = optuna.create_study()
|
||||
trial = study.ask()
|
||||
app = create_app(study._storage)
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/studies/{study._study_id}/{trial._trial_id}/note",
|
||||
"PUT",
|
||||
content_type="application/json",
|
||||
body=json.dumps(request_body),
|
||||
)
|
||||
return status, study
|
||||
|
||||
def test_save_trial_note_overwrite(self) -> None:
|
||||
study = optuna.create_study()
|
||||
trial = study.ask()
|
||||
app = create_app(study._storage)
|
||||
|
||||
def _get_request_body(note_version: int) -> dict[str, str | int]:
|
||||
return {"body": f"Test note ver. {note_version}.", "version": note_version}
|
||||
|
||||
for ver in range(1, 3):
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/studies/{study._study_id}/{trial._trial_id}/note",
|
||||
"PUT",
|
||||
content_type="application/json",
|
||||
body=json.dumps(_get_request_body(note_version=ver)),
|
||||
)
|
||||
assert status == 204
|
||||
# Check if the version 1 is deleted.
|
||||
expected_request_body = _get_request_body(note_version=2)
|
||||
expected_system_attrs = {
|
||||
note_ver_key(trial_id=0): expected_request_body["version"],
|
||||
f"{note_str_key_prefix(trial_id=0)}{0}": expected_request_body["body"],
|
||||
}
|
||||
for k, v in expected_system_attrs.items():
|
||||
assert k in study.system_attrs
|
||||
assert study.system_attrs[k] == v
|
||||
|
||||
def test_save_trial_note(self) -> None:
|
||||
request_body: dict[str, int | str] = {"body": "Test note.", "version": 1}
|
||||
status, study = self._save_trial_note(request_body)
|
||||
assert status == 204
|
||||
expected_system_attrs = {
|
||||
note_ver_key(0): request_body["version"],
|
||||
f"{note_str_key_prefix(0)}{0}": request_body["body"],
|
||||
}
|
||||
for k, v in expected_system_attrs.items():
|
||||
assert k in study.system_attrs
|
||||
assert study.system_attrs[k] == v
|
||||
|
||||
def test_save_trial_note_with_wrong_version(self) -> None:
|
||||
request_body: dict[str, int | str] = {"body": "Test note.", "version": 0}
|
||||
status, study = self._save_trial_note(request_body)
|
||||
assert status == 409
|
||||
assert note_ver_key(0) not in study.system_attrs
|
||||
|
||||
def test_save_trial_note_empty(self) -> None:
|
||||
status, study = self._save_trial_note(request_body={})
|
||||
assert status == 400
|
||||
assert note_ver_key(0) not in study.system_attrs
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
|
||||
def test_skip_trial(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = create_study(n_generate=4, storage=storage)
|
||||
@@ -238,6 +351,7 @@ class APITestCase(TestCase):
|
||||
assert len(best_trials) == 1
|
||||
assert best_trials[0].number == 2
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
|
||||
def test_remove_history(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = create_study(storage=storage, n_generate=3)
|
||||
@@ -271,6 +385,7 @@ class APITestCase(TestCase):
|
||||
assert histories[0]["is_removed"]
|
||||
assert len(study.get_preferences()) == 0
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
|
||||
def test_restore_history(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = create_study(storage=storage, n_generate=3)
|
||||
@@ -390,6 +505,125 @@ class APITestCase(TestCase):
|
||||
)
|
||||
self.assertEqual(status, 404)
|
||||
|
||||
def test_tell_trial_complete(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
trial_id = study.ask()._trial_id
|
||||
|
||||
app = create_app(storage)
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial_id}/tell",
|
||||
"POST",
|
||||
body=json.dumps(
|
||||
{
|
||||
"state": "Complete",
|
||||
"values": [0, 1, 2],
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 204)
|
||||
trial = storage.get_trial(trial_id)
|
||||
assert trial.state == optuna.trial.TrialState.COMPLETE
|
||||
assert trial.values == [0, 1, 2]
|
||||
|
||||
def test_tell_trial_fail(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
trial_id = study.ask()._trial_id
|
||||
|
||||
app = create_app(storage)
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial_id}/tell",
|
||||
"POST",
|
||||
body=json.dumps(
|
||||
{
|
||||
"state": "Fail",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 204)
|
||||
trial = storage.get_trial(trial_id)
|
||||
assert trial.state == optuna.trial.TrialState.FAIL
|
||||
|
||||
def test_tell_trial_with_no_state(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
trial_id = study.ask()._trial_id
|
||||
|
||||
app = create_app(storage)
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial_id}/tell",
|
||||
"POST",
|
||||
body=json.dumps({}),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
def test_tell_trial_with_invalid_state(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
for state in ["Pruned", "Running", "Waiting", "Invalid"]:
|
||||
trial_id = study.ask()._trial_id
|
||||
app = create_app(storage)
|
||||
with self.subTest(state=state):
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial_id}/tell",
|
||||
"POST",
|
||||
body=json.dumps(
|
||||
{
|
||||
"state": state,
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
def test_tell_trial_with_no_values(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
trial_id = study.ask()._trial_id
|
||||
|
||||
app = create_app(storage)
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial_id}/tell",
|
||||
"POST",
|
||||
body=json.dumps(
|
||||
{
|
||||
"state": "Complete",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
def test_tell_trial_with_invalid_values(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
for values in [1.0, ["foo"]]:
|
||||
trial_id = study.ask()._trial_id
|
||||
app = create_app(storage)
|
||||
with self.subTest(values=values):
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial_id}/tell",
|
||||
"POST",
|
||||
body=json.dumps(
|
||||
{
|
||||
"state": "Complete",
|
||||
"values": values,
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
|
||||
class BottleRequestHookTestCase(TestCase):
|
||||
def test_ignore_trailing_slashes(self) -> None:
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any
|
||||
from unittest import TestCase
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
import optuna
|
||||
from optuna import create_trial
|
||||
from optuna.distributions import BaseDistribution
|
||||
@@ -254,11 +255,29 @@ class _CachedExtraStudyPropertyUserAttrs(TestCase):
|
||||
|
||||
def test_infer_sortable(self) -> None:
|
||||
user_attrs_list: list[dict[str, Any]] = [
|
||||
{"a": 1, "b": 1, "c": 1, "d": "a", "e": 1, "f": True},
|
||||
{
|
||||
"a": 1,
|
||||
"b": 1,
|
||||
"c": 1,
|
||||
"d": "a",
|
||||
"e": 1,
|
||||
"f": True,
|
||||
"g": np.float128(1.1),
|
||||
"h": np.int64(2),
|
||||
},
|
||||
{"a": 2, "b": "a", "c": "a", "d": "a"},
|
||||
{"a": 3, "b": None, "c": 3, "d": "a", "e": 3},
|
||||
]
|
||||
expected = {"a": True, "b": False, "c": False, "d": False, "e": True, "f": False}
|
||||
expected = {
|
||||
"a": True,
|
||||
"b": False,
|
||||
"c": False,
|
||||
"d": False,
|
||||
"e": True,
|
||||
"f": False,
|
||||
"g": True,
|
||||
"h": True,
|
||||
}
|
||||
|
||||
trials = []
|
||||
for user_attrs in user_attrs_list:
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import optuna
|
||||
from optuna.trial import TrialState
|
||||
from optuna_dashboard._app import create_app
|
||||
import pytest
|
||||
|
||||
from .wsgi_client import send_request
|
||||
|
||||
|
||||
def _validate_output(
|
||||
storage: optuna.storages.BaseStorage,
|
||||
correct_status: int,
|
||||
study_id: int,
|
||||
expect_no_result: bool = False,
|
||||
extra_col_names: list[str] | None = None,
|
||||
) -> None:
|
||||
app = create_app(storage)
|
||||
status, _, body = send_request(
|
||||
app,
|
||||
f"/csv/{study_id}",
|
||||
"GET",
|
||||
content_type="application/json",
|
||||
)
|
||||
assert status == correct_status
|
||||
decoded_csv = str(body.decode("utf-8"))
|
||||
if expect_no_result:
|
||||
assert "is not found" in decoded_csv
|
||||
else:
|
||||
col_names = ["Number", "State"] + ([] if extra_col_names is None else extra_col_names)
|
||||
assert all(col_name in decoded_csv for col_name in col_names)
|
||||
|
||||
|
||||
def test_download_csv_no_trial() -> None:
|
||||
def objective(trial: optuna.Trial) -> float:
|
||||
x = trial.suggest_float("x", -100, 100)
|
||||
y = trial.suggest_categorical("y", [-1, 0, 1])
|
||||
return x**2 + y
|
||||
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
study.optimize(objective, n_trials=0)
|
||||
_validate_output(storage, 200, 0)
|
||||
|
||||
|
||||
def test_download_csv_all_waiting() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
study.add_trial(optuna.trial.create_trial(state=TrialState.WAITING))
|
||||
_validate_output(storage, 200, 0)
|
||||
|
||||
|
||||
def test_download_csv_all_running() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
study.add_trial(optuna.trial.create_trial(state=TrialState.RUNNING))
|
||||
_validate_output(storage, 200, 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("study_id", [0, 1])
|
||||
def test_download_csv_fail(study_id: int) -> None:
|
||||
def objective(trial: optuna.Trial) -> float:
|
||||
x = trial.suggest_float("x", -100, 100)
|
||||
y = trial.suggest_categorical("y", [-1, 0, 1])
|
||||
return x**2 + y
|
||||
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
optuna.logging.set_verbosity(optuna.logging.ERROR)
|
||||
study.optimize(objective, n_trials=10)
|
||||
expect_no_result = study_id != 0
|
||||
cols = ["Param x", "Param y", "Value"]
|
||||
_validate_output(storage, 404 if expect_no_result else 200, study_id, expect_no_result, cols)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_multi_obj", [True, False])
|
||||
def test_download_csv_multi_obj(is_multi_obj: bool) -> None:
|
||||
def objective(trial: optuna.Trial) -> Any:
|
||||
x = trial.suggest_float("x", -100, 100)
|
||||
y = trial.suggest_categorical("y", [-1, 0, 1])
|
||||
if is_multi_obj:
|
||||
return x**2, y
|
||||
return x**2 + y
|
||||
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
directions = ["minimize", "minimize"] if is_multi_obj else ["minimize"]
|
||||
study = optuna.create_study(storage=storage, directions=directions)
|
||||
optuna.logging.set_verbosity(optuna.logging.ERROR)
|
||||
study.optimize(objective, n_trials=10)
|
||||
cols = ["Param x", "Param y"]
|
||||
cols += ["Objective 0", "Objective 1"] if is_multi_obj else ["Value"]
|
||||
_validate_output(storage, 200, 0, extra_col_names=cols)
|
||||
|
||||
|
||||
def test_download_csv_user_attr() -> None:
|
||||
def objective(trial: optuna.Trial) -> float:
|
||||
x = trial.suggest_float("x", -100, 100)
|
||||
y = trial.suggest_categorical("y", [-1, 0, 1])
|
||||
trial.set_user_attr("abs_y", abs(y))
|
||||
return x**2 + y
|
||||
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
optuna.logging.set_verbosity(optuna.logging.ERROR)
|
||||
study.optimize(objective, n_trials=10)
|
||||
cols = ["Param x", "Param y", "Value", "UserAttribute abs_y"]
|
||||
_validate_output(storage, 200, 0, extra_col_names=cols)
|
||||
@@ -53,3 +53,25 @@ class NoteTestCase(TestCase):
|
||||
note_dict = note.get_note_from_system_attrs(system_attrs, trial._trial_id)
|
||||
self.assertEqual(note_dict["body"], body)
|
||||
self.assertEqual(note_dict["version"], expected_ver)
|
||||
|
||||
def test_copy_notes(self) -> None:
|
||||
old_study = optuna.create_study()
|
||||
old_trials = [
|
||||
old_study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) for _ in range(2)
|
||||
]
|
||||
storage = old_study._storage
|
||||
|
||||
notes = ["trial 0", "trial 1"]
|
||||
for trial, body in zip(old_trials, notes):
|
||||
save_note(trial, body)
|
||||
save_note(old_study, "Study")
|
||||
|
||||
new_study = optuna.create_study(storage=storage, directions=old_study.directions)
|
||||
new_study.add_trials(old_study.get_trials(deepcopy=False))
|
||||
|
||||
note.copy_notes(storage, old_study, new_study)
|
||||
system_attrs = new_study._storage.get_study_system_attrs(new_study._study_id)
|
||||
for new_trial, body in zip(new_study.get_trials(), notes):
|
||||
actual = note.get_note_from_system_attrs(system_attrs, new_trial._trial_id)
|
||||
self.assertEqual(actual["body"], body)
|
||||
self.assertEqual(get_note(new_study), "Study")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from typing import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -13,6 +14,7 @@ from optuna_dashboard._preferential_history import restore_history
|
||||
from optuna_dashboard._serializer import serialize_preference_history
|
||||
from optuna_dashboard.preferential import create_study
|
||||
from optuna_dashboard.preferential._system_attrs import _SYSTEM_ATTR_PREFIX_PREFERENCE
|
||||
import pytest
|
||||
|
||||
from .storage_supplier import parametrize_storages
|
||||
from .storage_supplier import StorageSupplier
|
||||
@@ -22,6 +24,10 @@ if TYPE_CHECKING:
|
||||
from optuna_dashboard._preferential_history import History
|
||||
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
pytest.skip("BoTorch dropped Python3.7 support", allow_module_level=True)
|
||||
|
||||
|
||||
@parametrize_storages
|
||||
def test_report_and_get_choices(storage_supplier: Callable[[], StorageSupplier]) -> None:
|
||||
with storage_supplier() as storage:
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import optuna
|
||||
from optuna_dashboard._serializer import serialize_attrs
|
||||
from optuna_dashboard._serializer import serialize_study_detail
|
||||
from optuna_dashboard._serializer import serialize_study_summary
|
||||
from optuna_dashboard._storage import get_study_summaries
|
||||
from optuna_dashboard.preferential import create_study
|
||||
import pytest
|
||||
|
||||
|
||||
def test_serialize_bytes() -> None:
|
||||
@@ -22,6 +26,33 @@ def test_serialize_dict() -> None:
|
||||
assert len(serialized) <= 1
|
||||
|
||||
|
||||
def test_serialize_numpy_integer() -> None:
|
||||
serialized = serialize_attrs(
|
||||
{
|
||||
"int8": np.int8(1),
|
||||
"int16": np.int16(1),
|
||||
"int32": np.int32(1),
|
||||
"int64": np.int64(1),
|
||||
}
|
||||
)
|
||||
assert len(serialized) == 4
|
||||
assert all([v["value"] == "1" for v in serialized])
|
||||
|
||||
|
||||
def test_serialize_numpy_floating() -> None:
|
||||
serialized = serialize_attrs(
|
||||
{
|
||||
"float16": np.float16(1.0),
|
||||
"float32": np.float32(1.0),
|
||||
"float64": np.float64(1.0),
|
||||
"float128": np.float128(1.0),
|
||||
}
|
||||
)
|
||||
assert len(serialized) == 4
|
||||
assert all([v["value"] == "1.0" for v in serialized])
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
|
||||
def test_get_study_detail_is_preferential() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = create_study(n_generate=4, storage=storage)
|
||||
@@ -48,6 +79,7 @@ def test_get_study_detail_is_not_preferential() -> None:
|
||||
assert not study_detail["is_preferential"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
|
||||
def test_get_study_summary_is_preferential() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
create_study(n_generate=4, storage=storage)
|
||||
|
||||
+13
-8
@@ -1,16 +1,20 @@
|
||||
use fanova::{FanovaOptions, RandomForestOptions};
|
||||
use js_sys::Array;
|
||||
use serde_wasm_bindgen::from_value;
|
||||
use fanova::{FanovaOptions, RandomForestOptions};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn wasm_fanova_calculate(features: Array, targets: Array) -> Vec<f64> {
|
||||
// TODO(c-bata): Fix error handling
|
||||
pub fn wasm_fanova_calculate(features: Array, targets: Array) -> Result<Vec<f64>, JsError> {
|
||||
let features_vec: Vec<Vec<f64>> = features
|
||||
.iter()
|
||||
.map(|x| from_value::<Vec<f64>>(x).unwrap())
|
||||
.collect();
|
||||
let targets_vec: Vec<f64> = targets.iter().map(|x| x.as_f64().unwrap()).collect();
|
||||
.map(|x| from_value::<Vec<f64>>(x))
|
||||
.collect::<Result<_, _>>()
|
||||
.map_err(|_| JsError::new("features must be of type number[][]"))?;
|
||||
let targets_vec: Vec<f64> = targets
|
||||
.iter()
|
||||
.map(|x| x.as_f64())
|
||||
.collect::<Option<_>>()
|
||||
.ok_or(JsError::new("targets must be of type number[]"))?;
|
||||
|
||||
let mut fanova = FanovaOptions::new()
|
||||
.random_forest(RandomForestOptions::new().seed(0))
|
||||
@@ -18,9 +22,10 @@ pub fn wasm_fanova_calculate(features: Array, targets: Array) -> Vec<f64> {
|
||||
features_vec.iter().map(|x| x.as_slice()).collect(),
|
||||
&targets_vec,
|
||||
)
|
||||
.unwrap();
|
||||
.map_err(|e| JsError::new(&format!("failed to build fANOVA model: {}", e)))?;
|
||||
let importances = (0..features_vec.len())
|
||||
.map(|i| fanova.quantify_importance(&[i]).mean)
|
||||
.collect::<Vec<_>>();
|
||||
return importances;
|
||||
|
||||
Ok(importances)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ interface DataGridColumn<T> {
|
||||
field: keyof T
|
||||
label: string
|
||||
sortable?: boolean
|
||||
less?: (a: T, b: T) => number
|
||||
less?: (a: T, b: T, ascending: boolean) => number
|
||||
filterable?: boolean
|
||||
toCellValue?: (rowIndex: number) => string | React.ReactNode
|
||||
padding?: "normal" | "checkbox" | "none"
|
||||
@@ -358,7 +358,10 @@ function stableSort<T>(
|
||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number])
|
||||
stabilizedThis.sort((a, b) => {
|
||||
if (less) {
|
||||
const result = order == "asc" ? -less(a[0], b[0]) : less(a[0], b[0])
|
||||
const ascending = order === "asc"
|
||||
const result = ascending
|
||||
? -less(a[0], b[0], ascending)
|
||||
: less(a[0], b[0], ascending)
|
||||
if (result !== 0) return result
|
||||
} else {
|
||||
const result = comparator(a[0], b[0])
|
||||
|
||||
@@ -39,6 +39,7 @@ export const PlotImportance: FC<{ study: Study }> = ({ study }) => {
|
||||
const values = filteredTrials.map(
|
||||
(t) => t.values?.[objectiveId] as number
|
||||
)
|
||||
// TODO: handle errors thrown by wasm_fanova_calculate
|
||||
const importance = wasm_fanova_calculate(features, values)
|
||||
return study.intersection_search_space.map((s, i) => ({
|
||||
name: s.name,
|
||||
|
||||
@@ -78,7 +78,7 @@ const plotIntermediateValue = (
|
||||
t.state === "Pruned" &&
|
||||
t.values &&
|
||||
t.values.length > 0) ||
|
||||
t.state == "Running"
|
||||
t.state === "Running"
|
||||
)
|
||||
const plotData: Partial<plotly.PlotData>[] = filteredTrials.map((trial) => {
|
||||
const values = trial.intermediate_values.filter(
|
||||
|
||||
@@ -20,12 +20,12 @@ export const TrialTable: FC<{
|
||||
},
|
||||
]
|
||||
|
||||
if (study === null || study.directions.length == 1) {
|
||||
if (study === null || study.directions.length === 1) {
|
||||
columns.push({
|
||||
field: "values",
|
||||
label: "Value",
|
||||
sortable: true,
|
||||
less: (firstEl, secondEl): number => {
|
||||
less: (firstEl, secondEl, ascending): number => {
|
||||
const firstVal = firstEl.values?.[0]
|
||||
const secondVal = secondEl.values?.[0]
|
||||
|
||||
@@ -33,9 +33,9 @@ export const TrialTable: FC<{
|
||||
return 0
|
||||
}
|
||||
if (firstVal === undefined) {
|
||||
return -1
|
||||
return ascending ? -1 : 1
|
||||
} else if (secondVal === undefined) {
|
||||
return 1
|
||||
return ascending ? 1 : -1
|
||||
}
|
||||
if (firstVal === "-inf" || secondVal === "inf") {
|
||||
return 1
|
||||
@@ -57,7 +57,7 @@ export const TrialTable: FC<{
|
||||
field: "values",
|
||||
label: `Objective ${objectiveId}`,
|
||||
sortable: true,
|
||||
less: (firstEl, secondEl): number => {
|
||||
less: (firstEl, secondEl, ascending): number => {
|
||||
const firstVal = firstEl.values?.[objectiveId]
|
||||
const secondVal = secondEl.values?.[objectiveId]
|
||||
|
||||
@@ -65,9 +65,9 @@ export const TrialTable: FC<{
|
||||
return 0
|
||||
}
|
||||
if (firstVal === undefined) {
|
||||
return -1
|
||||
return ascending ? -1 : 1
|
||||
} else if (secondVal === undefined) {
|
||||
return 1
|
||||
return ascending ? 1 : -1
|
||||
}
|
||||
if (firstVal === "-inf" || secondVal === "inf") {
|
||||
return 1
|
||||
@@ -96,7 +96,8 @@ export const TrialTable: FC<{
|
||||
null,
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
less: (firstEl, secondEl): number => {
|
||||
// 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
|
||||
@@ -126,7 +127,8 @@ export const TrialTable: FC<{
|
||||
?.value || null,
|
||||
sortable: attr_spec.sortable,
|
||||
filterable: false,
|
||||
less: (firstEl, secondEl): number => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
less: (firstEl, secondEl, _): number => {
|
||||
const firstVal = firstEl.user_attrs.find(
|
||||
(attr) => attr.key === attr_spec.key
|
||||
)?.value
|
||||
|
||||
@@ -64,7 +64,7 @@ const getSchemaVersion = (db: SQLite3DB): string => {
|
||||
|
||||
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
|
||||
if (schemaVersion === lowestVersion) return true
|
||||
return isGreaterSchemaVersion(schemaVersion, lowestVersion)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ const isGreaterSchemaVersion = (
|
||||
|
||||
const left = Number(leftVersion)
|
||||
const right = Number(rightVersion)
|
||||
if (left == right) return leftSuffix > rightSuffix
|
||||
if (left === right) return leftSuffix > rightSuffix
|
||||
return left > right
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ const getStudies = (db: SQLite3DB, schemaVersion: string): Study[] => {
|
||||
trials.forEach((trial) => {
|
||||
const userAttrs = getTrialUserAttributes(db, trial.trial_id)
|
||||
userAttrs.forEach((attr) => {
|
||||
if (union_user_attrs.findIndex((s) => s.key === attr.key) == -1) {
|
||||
if (union_user_attrs.findIndex((s) => s.key === attr.key) === -1) {
|
||||
union_user_attrs.push({ key: attr.key, sortable: false })
|
||||
}
|
||||
})
|
||||
@@ -116,7 +116,7 @@ const getStudies = (db: SQLite3DB, schemaVersion: string): Study[] => {
|
||||
params.forEach((param) => {
|
||||
param_names.add(param.name)
|
||||
if (
|
||||
union_search_space.findIndex((s) => s.name === param.name) == -1
|
||||
union_search_space.findIndex((s) => s.name === param.name) === -1
|
||||
) {
|
||||
union_search_space.push({ name: param.name })
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react"
|
||||
global.URL.createObjectURL = jest.fn()
|
||||
|
||||
import { cleanup, render, fireEvent } from "@testing-library/react"
|
||||
import { cleanup, render } from "@testing-library/react"
|
||||
import {
|
||||
DataGrid,
|
||||
DataGridColumn,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
// TODO(c-bata): Add tests to check filterChoices option
|
||||
it("Filter rows of DataGrid", () => {
|
||||
interface DummyAttribute {
|
||||
id: number
|
||||
@@ -23,7 +24,7 @@ it("Filter rows of DataGrid", () => {
|
||||
{ id: 5, key: "foo", value: 3 },
|
||||
]
|
||||
const columns: DataGridColumn<DummyAttribute>[] = [
|
||||
{ field: "key", label: "Key", filterable: true },
|
||||
{ field: "key", label: "Key" },
|
||||
{
|
||||
field: "value",
|
||||
label: "Value",
|
||||
@@ -39,46 +40,4 @@ it("Filter rows of DataGrid", () => {
|
||||
/>
|
||||
)
|
||||
expect(queryAllByText("bar").length).toBe(2)
|
||||
|
||||
// Filter rows by "foo"
|
||||
fireEvent.click(queryAllByText("foo")[0])
|
||||
expect(queryAllByText("foo").length).toBe(3)
|
||||
expect(queryAllByText("bar").length).toBe(0)
|
||||
})
|
||||
|
||||
it("Filter rows after sorted", () => {
|
||||
interface DummyAttribute {
|
||||
id: number
|
||||
key: string
|
||||
value: number
|
||||
}
|
||||
const dummyAttributes = [
|
||||
{ id: 1, key: "foo", value: 4000 },
|
||||
{ id: 2, key: "bar", value: 1000 },
|
||||
{ id: 3, key: "bar", value: 2000 },
|
||||
{ id: 4, key: "foo", value: 3000 },
|
||||
{ id: 5, key: "foo", value: 5000 },
|
||||
]
|
||||
const columns: DataGridColumn<DummyAttribute>[] = [
|
||||
{ field: "key", label: "Key", filterable: true },
|
||||
{
|
||||
field: "value",
|
||||
label: "Value",
|
||||
sortable: true,
|
||||
},
|
||||
]
|
||||
|
||||
const { getByText, queryAllByText } = render(
|
||||
<DataGrid<DummyAttribute>
|
||||
columns={columns}
|
||||
rows={dummyAttributes}
|
||||
keyField={"id"}
|
||||
/>
|
||||
)
|
||||
// Sort and filter rows
|
||||
fireEvent.click(getByText("Value"))
|
||||
fireEvent.click(queryAllByText("bar")[0])
|
||||
|
||||
expect(queryAllByText("1000").length).toBe(1)
|
||||
expect(queryAllByText("2000").length).toBe(1)
|
||||
})
|
||||
|
||||
@@ -14,6 +14,10 @@ Nothing to configure.
|
||||
|
||||
## Release Notes
|
||||
|
||||
### 0.1.0
|
||||
|
||||
Added older database schemas support (Optuna 2.6.0 or later)
|
||||
|
||||
### 0.0.1
|
||||
|
||||
Initial Release
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"displayName": "Optuna Dashboard",
|
||||
"description": "Web Dashboard for Optuna",
|
||||
"publisher": "Optuna",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"icon": "images/optuna-logo.png",
|
||||
"engines": {
|
||||
|
||||
Reference in New Issue
Block a user