diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-dashboard-tests.yml
similarity index 90%
rename from .github/workflows/e2e-tests.yml
rename to .github/workflows/e2e-dashboard-tests.yml
index 4665f676..c8912b9a 100644
--- a/.github/workflows/e2e-tests.yml
+++ b/.github/workflows/e2e-dashboard-tests.yml
@@ -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,4 @@ jobs:
run: playwright install
- name: Run e2e tests
- run: pytest e2e_tests
+ run: pytest e2e_tests/test_dashboard
diff --git a/.github/workflows/e2e-standalone-tests.yml b/.github/workflows/e2e-standalone-tests.yml
new file mode 100644
index 00000000..2de6d610
--- /dev/null
+++ b/.github/workflows/e2e-standalone-tests.yml
@@ -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
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index 8fee4e44..69923d04 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -6,6 +6,7 @@ on:
paths:
- '.github/workflows/python-tests.yml'
- '**.py'
+ - 'pyproject.toml'
jobs:
lint:
runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 62edd6c8..badea560 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,18 @@ $ docker run -it --rm -p 8080:8080 ghcr.io/optuna/optuna-dashboard postgresql+ps
+## Jupyter Lab Extension (Experimental)
+
+You can install the Jupyter Lab extension via [PyPI](https://pypi.org/project/jupyterlab-optuna/).
+
+```
+$ pip install jupyterlab jupyterlab-optuna
+```
+
+
+
+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)
@@ -93,7 +105,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).
-
+
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.
diff --git a/docs/_static/jupyterlab-extension.png b/docs/_static/jupyterlab-extension.png
new file mode 100644
index 00000000..7652e87f
Binary files /dev/null and b/docs/_static/jupyterlab-extension.png differ
diff --git a/docs/getting-started.rst b/docs/getting-started.rst
index 295dd55c..d3b040c6 100644
--- a/docs/getting-started.rst
+++ b/docs/getting-started.rst
@@ -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 `_.
+
+.. 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)
-----------------------------------
diff --git a/docs/tutorials/preferential-optimization.rst b/docs/tutorials/preferential-optimization.rst
index 4d2644ea..a192e883 100644
--- a/docs/tutorials/preferential-optimization.rst
+++ b/docs/tutorials/preferential-optimization.rst
@@ -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`_.
diff --git a/e2e_tests/test_usecases/__init__.py b/e2e_tests/test_dashboard/__init__.py
similarity index 100%
rename from e2e_tests/test_usecases/__init__.py
rename to e2e_tests/test_dashboard/__init__.py
diff --git a/e2e_tests/test_dashboard/test_usecases/__init__.py b/e2e_tests/test_dashboard/test_usecases/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/e2e_tests/test_usecases/test_study_history.py b/e2e_tests/test_dashboard/test_usecases/test_study_history.py
similarity index 97%
rename from e2e_tests/test_usecases/test_study_history.py
rename to e2e_tests/test_dashboard/test_usecases/test_study_history.py
index e37b8f5a..6c22b16b 100644
--- a/e2e_tests/test_usecases/test_study_history.py
+++ b/e2e_tests/test_dashboard/test_usecases/test_study_history.py
@@ -2,7 +2,7 @@ import optuna
from playwright.sync_api import Page
import pytest
-from ..test_server import make_test_server
+from ...test_server import make_test_server
def make_test_storage() -> optuna.storages.InMemoryStorage:
diff --git a/e2e_tests/visual_regression_test.py b/e2e_tests/test_dashboard/visual_regression_test.py
similarity index 99%
rename from e2e_tests/visual_regression_test.py
rename to e2e_tests/test_dashboard/visual_regression_test.py
index d83fbb5f..ea6ff930 100644
--- a/e2e_tests/visual_regression_test.py
+++ b/e2e_tests/test_dashboard/visual_regression_test.py
@@ -4,7 +4,7 @@ import optuna
from playwright.sync_api import Page
import pytest
-from .test_server import make_test_server
+from ..test_server import make_test_server
@pytest.fixture
diff --git a/e2e_tests/test_server.py b/e2e_tests/test_server.py
index b6d14734..a15acc68 100644
--- a/e2e_tests/test_server.py
+++ b/e2e_tests/test_server.py
@@ -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}"
diff --git a/e2e_tests/test_standalone/__init__.py b/e2e_tests/test_standalone/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/e2e_tests/test_standalone/test_study_list.py b/e2e_tests/test_standalone/test_study_list.py
new file mode 100644
index 00000000..b0d4735e
--- /dev/null
+++ b/e2e_tests/test_standalone/test_study_list.py
@@ -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.)"
diff --git a/optuna_dashboard/__init__.py b/optuna_dashboard/__init__.py
index ea2a8dbe..8b793e62 100644
--- a/optuna_dashboard/__init__.py
+++ b/optuna_dashboard/__init__.py
@@ -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.13.0"
diff --git a/optuna_dashboard/preferential/_study.py b/optuna_dashboard/preferential/_study.py
index d715b97b..101b61de 100644
--- a/optuna_dashboard/preferential/_study.py
+++ b/optuna_dashboard/preferential/_study.py
@@ -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.")
diff --git a/optuna_dashboard/ts/components/PreferenceHistory.tsx b/optuna_dashboard/ts/components/PreferenceHistory.tsx
index 6aa67317..b7bd04b4 100644
--- a/optuna_dashboard/ts/components/PreferenceHistory.tsx
+++ b/optuna_dashboard/ts/components/PreferenceHistory.tsx
@@ -147,6 +147,16 @@ const CandidateTrial: FC<{
overflow: "auto",
}}
>
+ setDetailShown(false)}
+ >
+
+
false}
diff --git a/optuna_dashboard/ts/components/StudyList.tsx b/optuna_dashboard/ts/components/StudyList.tsx
index c41c51cc..14eaa68b 100644
--- a/optuna_dashboard/ts/components/StudyList.tsx
+++ b/optuna_dashboard/ts/components/StudyList.tsx
@@ -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))
diff --git a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx
index 3c4e2e01..f04a269e 100644
--- a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx
+++ b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx
@@ -5,7 +5,9 @@ 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 { 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 (
@@ -32,7 +34,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 +47,11 @@ export const ThreejsArtifactViewer: React.FC = (
props
) => {
const [geometry, setGeometry] = useState([])
- const [modelSize, setModelSize] = useState(
- new THREE.Vector3(10, 10, 10)
+ const [boundingBox, setBoundingBox] = useState(
+ new THREE.Box3(
+ new THREE.Vector3(-10, -10, -10),
+ new THREE.Vector3(10, 10, 10)
+ )
)
const [cameraSettings, setCameraSettings] = useState(
new THREE.PerspectiveCamera()
@@ -54,11 +59,11 @@ export const ThreejsArtifactViewer: React.FC = (
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(() => {
@@ -81,20 +86,30 @@ export const ThreejsArtifactViewer: React.FC = (
}
})
}
- 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 (