Merge branch 'main' into add_color_scale_setting

This commit is contained in:
gen740 committed 2023-11-17 14:32:21 +09:00
commit 70752ddf51
48 files changed
+1295 -360

No files matched your search

@@ -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
@@ -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
+2 -1
View File
@@ -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: |
+1 -1
View File
@@ -24,6 +24,6 @@ jobs:
days-before-pr-close: 7 # default number
stale-issue-label: 'stale'
stale-pr-label: 'stale'
exempt-issue-labels: 'no-stale'
exempt-issue-labels: 'no-stale,good-first-issue,contribution-welcome'
exempt-pr-labels: 'no-stale'
operations-per-run: 1000
+13 -1
View File
@@ -77,6 +77,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 +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).
<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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

+1
View File
@@ -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
+13
View File
@@ -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)
-----------------------------------
+3 -3
View File
@@ -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`_.
File renamed without changes.
Whitespace-only changes.
@@ -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:
@@ -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
+28
View File
@@ -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}"
Whitespace-only changes.
@@ -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.)"
+1 -1
View File
@@ -17,4 +17,4 @@ from ._note import save_note # noqa
from ._preference_setting import register_preference_feedback_component # noqa
__version__ = "0.13.0b1"
__version__ = "0.13.0"
+35 -15
View File
@@ -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"
@@ -37,6 +41,10 @@ class PreferentialStudy:
To create and load a study, please refer to the documentation of
:func:`~optuna_dashboard.preferential.create_study` and
:func:`~optuna_dashboard.preferential.load_study` respectively.
.. note::
Preferential optimization is an experimental feature (introduced in v0.13.0).
The interface may change in newer versions without prior notice.
"""
def __init__(self, study: optuna.Study) -> None:
@@ -340,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
@@ -359,11 +366,19 @@ def create_study(
Returns:
A :class:`~optuna_dashboard.preferential.PreferentialStudy` object.
.. note::
Preferential optimization is an experimental feature (introduced in v0.13.0).
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(
@@ -433,18 +448,23 @@ 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.
.. note::
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.")
+102 -28
View File
@@ -4,7 +4,7 @@ import itertools
import math
from typing import Any
from typing import Callable
from typing import cast
import warnings
import botorch.acquisition.analytic
import botorch.models.model
@@ -17,6 +17,8 @@ import numpy as np
import optuna
import optuna._transform
from optuna.distributions import CategoricalDistribution
from optuna.distributions import FloatDistribution
from optuna.distributions import IntDistribution
import torch
from torch import Tensor
@@ -48,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)
@@ -156,6 +158,18 @@ def _truncnorm_mean_var_logz(alpha: Tensor) -> tuple[Tensor, Tensor, Tensor]:
return mean, var, logz
def _observation(var0: Tensor, mean0: Tensor, noise_var: Tensor) -> tuple[Tensor, Tensor, Tensor]:
obs_var = var0 + noise_var
obs_sigma = torch.sqrt(obs_var)
alpha = -mean0 / torch.clamp_min(obs_sigma, min=1e-20)
mean_norm, var_norm, logz = _truncnorm_mean_var_logz(alpha)
denom_factor = 1 / torch.clamp_min(noise_var + var_norm * var0, min=1e-20)
da = (1 - var_norm) * denom_factor
db = (mean0 * (1 - var_norm) + obs_sigma * mean_norm) * denom_factor
return (da, db, logz)
def _orthants_MVN_EP(
cov0: Tensor, preferences: Tensor, noise_var: Tensor, cycles: int
) -> tuple[Tensor, Tensor, Tensor]:
@@ -176,25 +190,17 @@ def _orthants_MVN_EP(
r0 = (1 - var1 * virtual_obs_a[i]).reciprocal()
var0 = var1 * r0
mean0 = (mean1 + var1 * virtual_obs_b[i]) * r0
mean0 = (mean1 - var1 * virtual_obs_b[i]) * r0
obs_var = var0 + noise_var
obs_sigma = torch.sqrt(obs_var)
alpha = -mean0 / torch.clamp_min(obs_sigma, min=1e-20)
mean_norm, var_norm, logz = _truncnorm_mean_var_logz(alpha)
virtual_obs_a2, virtual_obs_b2, logz = _observation(var0, mean0, noise_var)
kalman_factor = var0 / torch.clamp_min(obs_var, min=1e-20)
mean2 = mean0 + obs_sigma * mean_norm * kalman_factor
var2 = kalman_factor * (noise_var + var_norm * var0)
var1_var2_inv = torch.clamp_min(var1 * var2, min=1e-20).reciprocal()
db = (mean1 * var2 - mean2 * var1) * var1_var2_inv
da = (var1 - var2) * var1_var2_inv
virtual_obs_b[i] = virtual_obs_b[i] + db
virtual_obs_a[i] = virtual_obs_a[i] + da
da = virtual_obs_a2 - virtual_obs_a[i]
db = virtual_obs_b2 - virtual_obs_b[i]
virtual_obs_a[i] = virtual_obs_a2
virtual_obs_b[i] = virtual_obs_b2
dr = (1 + var1 * da).reciprocal()
mu = mu - Sxy * ((db + mean1 * da) * dr)
mu = mu + Sxy * ((db - mean1 * da) * dr)
cov = cov - (Sxy[:, None] * (da * dr)) @ Sxy[None, :]
log_zs[i] = logz
return mu, cov, torch.sum(log_zs)
@@ -278,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,
*,
@@ -359,11 +385,54 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler):
)
# TODO: Make it possible to apply it on mixed search space
if all(isinstance(dist, CategoricalDistribution) for dist in search_space.values()):
def get_all_possible_params(dist: optuna.distributions.BaseDistribution) -> list[Any]:
if isinstance(dist, CategoricalDistribution):
return list(dist.choices)
elif isinstance(dist, (IntDistribution, FloatDistribution)):
return list(np.arange(dist.low, dist.high, dist.step))
else:
return []
all_possible_params = {
name: get_all_possible_params(dist) for name, dist in search_space.items()
}
is_all_discrete = all(
len(possible_params) > 0 for possible_params in all_possible_params.values()
)
search_space_size = np.prod(
[len(possible_params) for possible_params in all_possible_params.values()]
)
# TODO(contramundum53): Fix this arbitrarily chosen limit.
size_limit = 1e6
can_evaluate_all = is_all_discrete and search_space_size <= size_limit
if (
any(isinstance(dist, CategoricalDistribution) for dist in search_space.values())
and not can_evaluate_all
):
if is_all_discrete:
warnings.warn(
"The objective function has categorical parameters, "
"but the total search space is too large to be enumerated. "
f"(Search space size: {search_space_size} > limit: {size_limit})"
"This may result in significantly bad performance."
)
else:
warnings.warn(
"The objective function has categorical parameters, "
"but the search space cannot be enumerated because "
"it also contains continuous parameters. "
"This may result in significantly bad performance. "
"You can work around this problem by specifying 'step' "
"in each continuous parameter."
)
if is_all_discrete and can_evaluate_all:
all_param_combinations = itertools.product(
*[
[(name, choice) for choice in cast(CategoricalDistribution, dist).choices]
for name, dist in search_space.items()
[(name, choice) for choice in possible_params]
for name, possible_params in all_possible_params.items()
]
)
choices = torch.tensor(
@@ -395,6 +464,11 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler):
param_name: str,
param_distribution: optuna.distributions.BaseDistribution,
) -> Any:
warnings.warn(
"Dynamic search space detected. "
f"Falling back to {self.independent_sampler.__class__.__name__}."
)
return self.independent_sampler.sample_independent(
study, trial, param_name, param_distribution
)
+2 -2
View File
@@ -17,7 +17,7 @@ import ListItemText from "@mui/material/ListItemText"
import {
drawerOpenState,
reloadIntervalState,
useStudyIsPreferencial,
useStudyIsPreferential,
} from "../state"
import { Link } from "react-router-dom"
import AutoGraphIcon from "@mui/icons-material/AutoGraph"
@@ -133,7 +133,7 @@ export const AppDrawer: FC<{
const [open, setOpen] = useRecoilState<boolean>(drawerOpenState)
const reloadInterval = useRecoilValue<number>(reloadIntervalState)
const isPreferential =
studyId !== undefined ? useStudyIsPreferencial(studyId) : null
studyId !== undefined ? useStudyIsPreferential(studyId) : null
const styleListItem = {
display: "block",
@@ -21,6 +21,18 @@ export const ArtifactCardMedia: FC<{
filetype={artifact.filename.split(".").pop()}
/>
)
} else if (artifact.mimetype.startsWith("video")) {
return (
<video
controls
style={{
width: "100%",
height: "auto",
}}
>
<source src={urlPath} type={artifact.mimetype} />
</video>
)
} else if (artifact.mimetype.startsWith("audio")) {
return (
<audio controls>
+5 -2
View File
@@ -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])
+32 -14
View File
@@ -174,19 +174,19 @@ const plotContour = (
return
}
const xAxis = getAxisInfo(study, trials, xParam)
const yAxis = getAxisInfo(study, trials, yParam)
const xAxis = getAxisInfo(trials, xParam)
const yAxis = getAxisInfo(trials, yParam)
const xIndices = xAxis.indices
const yIndices = yAxis.indices
const layout: Partial<plotly.Layout> = {
xaxis: {
title: xParam.name,
type: xAxis.isCat ? "category" : undefined,
type: xAxis.isCat ? "category" : xAxis.isLog ? "log" : "linear",
},
yaxis: {
title: yParam.name,
type: yAxis.isCat ? "category" : undefined,
type: yAxis.isCat ? "category" : yAxis.isLog ? "log" : "linear",
},
margin: {
l: 50,
@@ -207,12 +207,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)
@@ -246,12 +250,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
@@ -290,9 +302,19 @@ const getAxisInfoForNumericalParams = (
paramName: string,
distribution: FloatDistribution | IntDistribution
): AxisInfo => {
const padding = (distribution.high - distribution.low) * PADDING_RATIO
const min = distribution.low - padding
const max = distribution.high + padding
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) =>
@@ -353,11 +375,7 @@ const getAxisInfoForCategoricalParams = (
}
}
const getAxisInfo = (
study: StudyDetail,
trials: Trial[],
param: SearchSpaceItem
): AxisInfo => {
const getAxisInfo = (trials: Trial[], param: SearchSpaceItem): AxisInfo => {
if (param.distribution.type === "CategoricalDistribution") {
return getAxisInfoForCategoricalParams(
trials,
@@ -89,16 +89,21 @@ const plotIntermediateValue = (
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)
@@ -181,6 +181,21 @@ const plotCoordinate = (
.join("")
}
const calculateLogScale = (values: number[]) => {
const logValues = values.map((v) => {
return Math.log10(v)
})
const minValue = Math.min(...logValues)
const maxValue = Math.max(...logValues)
const range = [Math.floor(minValue), Math.ceil(maxValue)]
const tickvals = Array.from(
{ length: Math.ceil(maxValue) - Math.floor(minValue) + 1 },
(_, i) => i + Math.floor(minValue)
)
const ticktext = tickvals.map((x) => `${Math.pow(10, x).toPrecision(3)}`)
return { logValues, range, tickvals, ticktext }
}
const dimensions = targets.map((target) => {
if (target.kind === "objective" || target.kind === "user_attr") {
const values: number[] = trials.map(
@@ -199,13 +214,7 @@ const plotCoordinate = (
const values: number[] = trials.map(
(t) => target.getTargetValue(t) as number
)
if (s.distribution.type !== "CategoricalDistribution") {
return {
label: breakLabelIfTooLong(s.name),
values: values,
range: [s.distribution.low, s.distribution.high],
}
} else {
if (s.distribution.type === "CategoricalDistribution") {
// categorical
const vocabArr: string[] = s.distribution.choices.map((c) => c.value)
const tickvals: number[] = vocabArr.map((v, i) => i)
@@ -217,6 +226,24 @@ const plotCoordinate = (
tickvals: tickvals,
ticktext: vocabArr,
}
} else if (s.distribution.log) {
// numerical and log
const { logValues, range, tickvals, ticktext } =
calculateLogScale(values)
return {
label: breakLabelIfTooLong(s.name),
values: logValues,
range,
tickvals,
ticktext,
}
} else {
// numerical and linear
return {
label: breakLabelIfTooLong(s.name),
values: values,
range: [s.distribution.low, s.distribution.high],
}
}
}
})
@@ -0,0 +1,414 @@
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 { makeHovertext } from "../graphUtil"
import { useMergedUnionSearchSpace } from "../searchSpace"
const PADDING_RATIO = 0.05
const plotDomId = "graph-rank"
interface AxisInfo {
name: string
range: [number, number]
isLog: boolean
isCat: boolean
}
interface RankPlotInfo {
xaxis: AxisInfo
yaxis: AxisInfo
xvalues: (string | number)[]
yvalues: (string | number)[]
zvalues: 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)
const xValues: (string | number)[] = []
const yValues: (string | number)[] = []
const zValues: number[] = []
const isFeasible: boolean[] = []
const hovertext: string[] = []
filteredTrials.forEach((trial) => {
const xValue =
trial.params.find((p) => p.name === xAxis.name)?.param_external_value ||
null
const yValue =
trial.params.find((p) => p.name === yAxis.name)?.param_external_value ||
null
if (trial.values === undefined || xValue === null || yValue === null) {
return
}
const zValue = Number(trial.values[objectiveId])
const feasibility = trial.constraints.every((c) => c <= 0)
xValues.push(xValue)
yValues.push(yValue)
zValues.push(zValue)
isFeasible.push(feasibility)
hovertext.push(makeHovertext(trial))
})
const colors = getColors(zValues)
return {
xaxis: xAxis,
yaxis: yAxis,
xvalues: xValues,
yvalues: yValues,
zvalues: zValues,
colors,
is_feasible: isFeasible,
hovertext,
}
}
const filterFunc = (trial: Trial): boolean => {
return trial.state === "Complete" && trial.values !== undefined
}
const getAxisInfo = (trials: Trial[], param: SearchSpaceItem): AxisInfo => {
if (param.distribution.type === "CategoricalDistribution") {
return getAxisInfoForCategorical(trials, param.name, param.distribution)
} else {
return getAxisInfoForNumerical(trials, param.name, param.distribution)
}
}
const getAxisInfoForCategorical = (
trials: Trial[],
param: string,
distribution: CategoricalDistribution
): AxisInfo => {
const values = trials.map(
(trial) =>
trial.params.find((p) => p.name === param)?.param_internal_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
return {
name: param,
range: [min, max],
isLog: false,
isCat: true,
}
}
const getAxisInfoForNumerical = (
trials: Trial[],
param: string,
distribution: FloatDistribution | IntDistribution
): AxisInfo => {
const values = trials.map(
(trial) =>
trial.params.find((p) => p.name === param)?.param_internal_value || null
)
const nonNullValues: number[] = []
values.forEach((value) => {
if (value !== null) {
nonNullValues.push(value)
}
})
let min = Math.min(...nonNullValues)
let max = Math.max(...nonNullValues)
if (distribution.log) {
const padding = (Math.log10(max) - Math.log10(min)) * PADDING_RATIO
min = Math.pow(10, Math.log10(min) - padding)
max = Math.pow(10, Math.log10(max) + padding)
} else {
const padding = (max - min) * PADDING_RATIO
min = min - padding
max = max + padding
}
return {
name: param,
range: [min, max],
isLog: distribution.log,
isCat: false,
}
}
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()
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 xAxis = rankPlotInfo.xaxis
const yAxis = rankPlotInfo.yaxis
const layout: Partial<plotly.Layout> = {
xaxis: {
title: xAxis.name,
type: xAxis.isCat ? "category" : xAxis.isLog ? "log" : "linear",
},
yaxis: {
title: yAxis.name,
type: yAxis.isCat ? "category" : yAxis.isLog ? "log" : "linear",
},
margin: {
l: 50,
t: 0,
r: 50,
b: 50,
},
uirevision: "true",
template: mode === "dark" ? plotlyDarkTemplate : {},
}
let xValues = rankPlotInfo.xvalues
let yValues = rankPlotInfo.yvalues
if (xAxis.isCat && !yAxis.isCat) {
const xIndices: number[] = Array.from(Array(xValues.length).keys()).sort(
(a, b) =>
xValues[a]
.toString()
.toLowerCase()
.localeCompare(xValues[b].toString().toLowerCase())
)
xValues = xIndices.map((i) => xValues[i])
yValues = xIndices.map((i) => yValues[i])
}
if (!xAxis.isCat && yAxis.isCat) {
const yIndices: number[] = Array.from(Array(yValues.length).keys()).sort(
(a, b) =>
yValues[a]
.toString()
.toLowerCase()
.localeCompare(yValues[b].toString().toLowerCase())
)
xValues = yIndices.map((i) => xValues[i])
yValues = yIndices.map((i) => yValues[i])
}
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])
}
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)
}
+51 -45
View File
@@ -193,65 +193,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",
@@ -260,6 +266,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)
}
@@ -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}
@@ -18,7 +18,7 @@ import { actionCreator } from "../action"
import {
reloadIntervalState,
useStudyDetailValue,
useStudyIsPreferencial,
useStudyIsPreferential,
useStudyName,
} from "../state"
import { TrialTable } from "./TrialTable"
@@ -27,6 +27,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"
@@ -55,7 +56,7 @@ export const StudyDetail: FC<{
const studyDetail = useStudyDetailValue(studyId)
const reloadInterval = useRecoilValue<number>(reloadIntervalState)
const studyName = useStudyName(studyId)
const isPreferential = useStudyIsPreferencial(studyId)
const isPreferential = useStudyIsPreferential(studyId)
const title =
studyName !== null ? `${studyName} (id=${studyId})` : `Study #${studyId}`
@@ -122,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>
+1 -2
View File
@@ -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))
@@ -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<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,11 +59,11 @@ 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(() => {
@@ -81,20 +86,30 @@ export const ThreejsArtifactViewer: React.FC<ThreejsArtifactViewerProps> = (
}
})
}
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
camera={cameraSettings}
@@ -102,7 +117,7 @@ export const ThreejsArtifactViewer: React.FC<ThreejsArtifactViewerProps> = (
>
<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 +136,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 +162,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`}
+10 -8
View File
@@ -28,7 +28,7 @@ export const TrialTable: FC<{
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
@@ -106,7 +106,8 @@ export const TrialTable: FC<{
?.param_external_value || null,
sortable: sortable,
filterable: filterable,
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
@@ -146,7 +147,8 @@ export const TrialTable: FC<{
?.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
+1 -1
View File
@@ -95,7 +95,7 @@ export const useStudyDirections = (
return studyDetail?.directions || studySummary?.directions || null
}
export const useStudyIsPreferencial = (studyId: number): boolean | null => {
export const useStudyIsPreferential = (studyId: number): boolean | null => {
const studyDetail = useStudyDetailValue(studyId)
const studySummary = useStudySummaryValue(studyId)
return studyDetail?.is_preferential || studySummary?.is_preferential || null
+130 -126
View File
@@ -75,11 +75,12 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
"integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
"version": "7.22.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
"integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"dependencies": {
"@babel/highlight": "^7.18.6"
"@babel/highlight": "^7.22.13",
"chalk": "^2.4.2"
},
"engines": {
"node": ">=6.9.0"
@@ -125,13 +126,14 @@
}
},
"node_modules/@babel/generator": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz",
"integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
"integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dev": true,
"dependencies": {
"@babel/types": "^7.20.5",
"@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"engines": {
@@ -250,9 +252,9 @@
}
},
"node_modules/@babel/helper-environment-visitor": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
"integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -271,25 +273,25 @@
}
},
"node_modules/@babel/helper-function-name": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
"integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"dependencies": {
"@babel/template": "^7.18.10",
"@babel/types": "^7.19.0"
"@babel/template": "^7.22.15",
"@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-hoist-variables": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
"integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
"integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
"dev": true,
"dependencies": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
@@ -417,29 +419,29 @@
}
},
"node_modules/@babel/helper-split-export-declaration": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
"integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dev": true,
"dependencies": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.19.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
"integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"engines": {
"node": ">=6.9.0"
}
@@ -483,12 +485,12 @@
}
},
"node_modules/@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
"integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"dependencies": {
"@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
@@ -496,9 +498,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
"integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -1657,33 +1659,33 @@
}
},
"node_modules/@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
"integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==",
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/parser": "^7.18.10",
"@babel/types": "^7.18.10"
"@babel/code-frame": "^7.22.13",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz",
"integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==",
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.5",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.20.5",
"@babel/types": "^7.20.5",
"@babel/code-frame": "^7.22.13",
"@babel/generator": "^7.23.0",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.0",
"@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -1692,12 +1694,12 @@
}
},
"node_modules/@babel/types": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
"integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dependencies": {
"@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1",
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
@@ -12511,9 +12513,9 @@
"integrity": "sha512-2b7w4CQI06px8HVpKpgZtfuoDjuCLA26VlgdnG71UDBrJvtCYvXb39H4ElNv+CA1bbD4S98KanpWPRqTqlxBZw=="
},
"node_modules/postcss": {
"version": "8.4.29",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
"integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==",
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true,
"funding": [
{
@@ -15313,11 +15315,12 @@
}
},
"@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
"integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
"version": "7.22.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
"integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"requires": {
"@babel/highlight": "^7.18.6"
"@babel/highlight": "^7.22.13",
"chalk": "^2.4.2"
}
},
"@babel/compat-data": {
@@ -15350,13 +15353,14 @@
}
},
"@babel/generator": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz",
"integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
"integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dev": true,
"requires": {
"@babel/types": "^7.20.5",
"@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
},
"dependencies": {
@@ -15444,9 +15448,9 @@
}
},
"@babel/helper-environment-visitor": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
"integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true
},
"@babel/helper-explode-assignable-expression": {
@@ -15459,22 +15463,22 @@
}
},
"@babel/helper-function-name": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
"integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"requires": {
"@babel/template": "^7.18.10",
"@babel/types": "^7.19.0"
"@babel/template": "^7.22.15",
"@babel/types": "^7.23.0"
}
},
"@babel/helper-hoist-variables": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
"integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
"integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
"dev": true,
"requires": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.22.5"
}
},
"@babel/helper-member-expression-to-functions": {
@@ -15569,23 +15573,23 @@
}
},
"@babel/helper-split-export-declaration": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
"integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
"version": "7.22.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dev": true,
"requires": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.22.5"
}
},
"@babel/helper-string-parser": {
"version": "7.19.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
"integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw=="
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw=="
},
"@babel/helper-validator-identifier": {
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w=="
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A=="
},
"@babel/helper-validator-option": {
"version": "7.18.6",
@@ -15617,19 +15621,19 @@
}
},
"@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
"integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"requires": {
"@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
}
},
"@babel/parser": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
"integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"dev": true
},
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
@@ -16401,41 +16405,41 @@
}
},
"@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
"integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==",
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/parser": "^7.18.10",
"@babel/types": "^7.18.10"
"@babel/code-frame": "^7.22.13",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15"
}
},
"@babel/traverse": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz",
"integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==",
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.5",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.20.5",
"@babel/types": "^7.20.5",
"@babel/code-frame": "^7.22.13",
"@babel/generator": "^7.23.0",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.0",
"@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
"integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"requires": {
"@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1",
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
}
},
@@ -24278,9 +24282,9 @@
"integrity": "sha512-2b7w4CQI06px8HVpKpgZtfuoDjuCLA26VlgdnG71UDBrJvtCYvXb39H4ElNv+CA1bbD4S98KanpWPRqTqlxBZw=="
},
"postcss": {
"version": "8.4.29",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
"integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==",
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true,
"requires": {
"nanoid": "^3.3.6",
+5 -1
View File
@@ -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"
@@ -0,0 +1,29 @@
import sys
from unittest.mock import patch
import numpy as np
import pytest
if sys.version_info >= (3, 8):
from optuna_dashboard.preferential.samplers.gp import _one_side_trunc_norm_sampling
import torch
else:
pytest.skip("BoTorch dropped Python3.7 support", allow_module_level=True)
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)
+5
View File
@@ -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:
+9
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import sys
from unittest import TestCase
import optuna
@@ -14,6 +15,7 @@ from optuna_dashboard._preferential_history import remove_history
from optuna_dashboard._preferential_history import report_history
from optuna_dashboard._serializer import serialize_preference_history
from optuna_dashboard.preferential import create_study
import pytest
from .wsgi_client import send_request
@@ -105,6 +107,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 +131,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 +165,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 +189,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 +220,7 @@ class APITestCase(TestCase):
assert study_detail["feedback_component_type"]["output_type"] == "artifact"
assert study_detail["feedback_component_type"]["artifact_key"] == "image"
@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 +245,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 +279,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)
@@ -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:
+5
View File
@@ -1,11 +1,14 @@
from __future__ import annotations
import sys
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 +25,7 @@ def test_serialize_dict() -> None:
assert len(serialized) <= 1
@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 +52,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
View File
@@ -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)
}
+5 -2
View File
@@ -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,
+10 -8
View File
@@ -25,7 +25,7 @@ export const TrialTable: FC<{
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
+155 -50
View File
@@ -38,10 +38,11 @@ export const loadStorage = (
)
db.checkRc(rc)
try {
if (!isSupportedSchema(db)) {
const schemaVersion = getSchemaVersion(db)
if (!isSupportedSchema(schemaVersion)) {
return
}
const studies = getStudies(db)
const studies = getStudies(db, schemaVersion)
setter((prev) => [...prev, ...studies])
} finally {
db.close()
@@ -49,21 +50,41 @@ export const loadStorage = (
})
}
const isSupportedSchema = (db: SQLite3DB): boolean => {
let supported = true
const getSchemaVersion = (db: SQLite3DB): string => {
let schemaVersion = ""
db.exec({
sql: "SELECT schema_version FROM version_info LIMIT 1",
sql: "SELECT version_num FROM alembic_version LIMIT 1",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
if (vals[0] != 12) {
supported = false
}
schemaVersion = vals[0]
},
})
return supported
return schemaVersion
}
const getStudies = (db: SQLite3DB): Study[] => {
const isSupportedSchema = (schemaVersion: string): boolean => {
const lowestVersion = "v2.6.0.a" // supported: "v3.2.0.a", "v3.0.0.{a,b,c,d}", "v2.6.0.a"
if (schemaVersion == lowestVersion) return true
return isGreaterSchemaVersion(schemaVersion, lowestVersion)
}
const isGreaterSchemaVersion = (
leftVersion: string,
rightVersion: string
): boolean => {
// return leftVersion > rightVersion
const leftSuffix = leftVersion.split(".").reverse()[0]
const rightSuffix = rightVersion.split(".").reverse()[0]
leftVersion = leftVersion.replace(/\D/g, "")
rightVersion = rightVersion.replace(/\D/g, "")
const left = Number(leftVersion)
const right = Number(rightVersion)
if (left == right) return leftSuffix > rightSuffix
return left > right
}
const getStudies = (db: SQLite3DB, schemaVersion: string): Study[] => {
const studies: Study[] = []
db.exec({
sql:
@@ -78,7 +99,7 @@ const getStudies = (db: SQLite3DB): Study[] => {
vals[2] === "MINIMIZE" ? "minimize" : "maximize"
const objective = vals[3]
const trials = getTrials(db, studyId)
const trials = getTrials(db, studyId, schemaVersion)
const union_search_space: SearchSpaceItem[] = []
const union_user_attrs: AttributeSpec[] = []
let intersection_search_space: Set<SearchSpaceItem> = new Set()
@@ -136,7 +157,11 @@ const getStudies = (db: SQLite3DB): Study[] => {
return studies
}
const getTrials = (db: SQLite3DB, studyId: number): Trial[] => {
const getTrials = (
db: SQLite3DB,
studyId: number,
schemaVersion: string
): Trial[] => {
const trials: Trial[] = []
db.exec({
sql:
@@ -160,8 +185,12 @@ const getTrials = (db: SQLite3DB, studyId: number): Trial[] => {
number: vals[1],
study_id: studyId,
state: state,
values: getTrialValues(db, trialId),
intermediate_values: getTrialIntermediateValues(db, trialId),
values: getTrialValues(db, trialId, schemaVersion),
intermediate_values: getTrialIntermediateValues(
db,
trialId,
schemaVersion
),
params: [], // Set this column later
user_attrs: [], // Set this column later
datetime_start: vals[3],
@@ -173,24 +202,41 @@ const getTrials = (db: SQLite3DB, studyId: number): Trial[] => {
return trials
}
const getTrialValues = (db: SQLite3DB, trialId: number): TrialValueNumber[] => {
const getTrialValues = (
db: SQLite3DB,
trialId: number,
schemaVersion: string
): TrialValueNumber[] => {
const values: TrialValueNumber[] = []
db.exec({
sql:
"SELECT value, value_type" +
` FROM trial_values WHERE trial_id = ${trialId}` +
" ORDER BY objective",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
values.push(
vals[1] === "INF_NEG"
? "-inf"
: vals[1] === "INF_POS"
? "+inf"
: vals[0]
)
},
})
if (isGreaterSchemaVersion(schemaVersion, "v3.0.0.c")) {
db.exec({
sql:
"SELECT value, value_type" +
` FROM trial_values WHERE trial_id = ${trialId}` +
" ORDER BY objective",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
values.push(
vals[1] === "INF_NEG"
? "-inf"
: vals[1] === "INF_POS"
? "+inf"
: vals[0]
)
},
})
} else {
db.exec({
sql:
"SELECT value" +
` FROM trial_values WHERE trial_id = ${trialId}` +
" ORDER BY objective",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
values.push(vals[0])
},
})
}
return values
}
@@ -241,6 +287,30 @@ const parseDistributionJSON = (t: string): Distribution => {
step: parsed.attributes.step as number,
log: parsed.attributes.log as boolean,
}
} else if (parsed.name === "UniformDistribution") {
return {
type: "FloatDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: null,
log: false,
}
} else if (parsed.name === "LogUniformDistribution") {
return {
type: "FloatDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: null,
log: true,
}
} else if (parsed.name === "DiscreteUniformDistribution") {
return {
type: "FloatDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: parsed.attributes.q,
log: false,
}
} else if (parsed.name === "IntDistribution") {
return {
type: "IntDistribution",
@@ -249,6 +319,22 @@ const parseDistributionJSON = (t: string): Distribution => {
step: parsed.attributes.step as number,
log: parsed.attributes.log as boolean,
}
} else if (parsed.name === "IntUniformDistribution") {
return {
type: "IntDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: parsed.attributes.step as number,
log: false,
}
} else if (parsed.name === "IntLogUniformDistribution") {
return {
type: "IntDistribution",
low: parsed.attributes.low as number,
high: parsed.attributes.high as number,
step: parsed.attributes.step as number,
log: true,
}
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const choices = parsed.attributes.choices.map((value: any) => {
@@ -287,26 +373,45 @@ const getTrialUserAttributes = (
const getTrialIntermediateValues = (
db: SQLite3DB,
trialId: number
trialId: number,
schemaVersion: string
): TrialIntermediateValue[] => {
const values: TrialIntermediateValue[] = []
db.exec({
sql:
"SELECT step, intermediate_value, intermediate_value_type" +
` FROM trial_intermediate_values WHERE trial_id = ${trialId}` +
" ORDER BY step",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
values.push({
step: vals[0],
value:
vals[2] === "INF_NEG"
? "-inf"
: vals[2] === "INF_POS"
? "+inf"
: vals[1],
})
},
})
if (isGreaterSchemaVersion(schemaVersion, "v3.0.0.c")) {
db.exec({
sql:
"SELECT step, intermediate_value, intermediate_value_type" +
` FROM trial_intermediate_values WHERE trial_id = ${trialId}` +
" ORDER BY step",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
values.push({
step: vals[0],
value:
vals[2] === "INF_NEG"
? "-inf"
: vals[2] === "INF_POS"
? "+inf"
: vals[2] === "NAN"
? "nan"
: vals[1],
})
},
})
} else {
db.exec({
sql:
"SELECT step, intermediate_value" +
` FROM trial_intermediate_values WHERE trial_id = ${trialId}` +
" ORDER BY step",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (vals: any[]) => {
values.push({
step: vals[0],
value: vals[1],
})
},
})
}
return values
}
+2 -2
View File
@@ -10,7 +10,7 @@ type FloatDistribution = {
type: "FloatDistribution"
low: number
high: number
step: number
step: number | null
log: boolean
}
@@ -18,7 +18,7 @@ type IntDistribution = {
type: "IntDistribution"
low: number
high: number
step: number
step: number | null
log: boolean
}
+4
View File
@@ -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
View File
@@ -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": {