mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-10 12:23:22 +08:00
Merge branch 'main' into refactor-datagrid-filter
This commit is contained in:
+2
-1
@@ -5,7 +5,8 @@ module.exports = {
|
||||
'@typescript-eslint',
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/ban-ts-comment": "off"
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"eqeqeq": ["error", "smart"],
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
|
||||
@@ -47,4 +47,10 @@ jobs:
|
||||
run: playwright install
|
||||
|
||||
- name: Run e2e tests
|
||||
run: pytest e2e_tests/test_dashboard
|
||||
run: |
|
||||
if [ "${{ matrix.optuna-version }}" = "optuna==2.10.0" ]; then
|
||||
ignore_option="--ignore e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py"
|
||||
else
|
||||
ignore_option=""
|
||||
fi
|
||||
pytest e2e_tests/test_dashboard $ignore_option
|
||||
|
||||
@@ -83,6 +83,7 @@ $ pytest python_tests/
|
||||
|
||||
```
|
||||
$ pip install -r requirements.txt
|
||||
$ playwright install
|
||||
$ pytest e2e_tests
|
||||
```
|
||||
|
||||
@@ -92,6 +93,9 @@ If you want to create a screenshot for each test, please run a following command
|
||||
$ pytest e2e_tests --screenshot on --output tmp
|
||||
```
|
||||
|
||||
If you want to generate a locator in each webpage, please use the playwright codegen. See [this page](https://playwright.dev/python/docs/codegen-intro) for more details.
|
||||
|
||||
|
||||
For more detail options, you can check [this page](https://playwright.dev/python/docs/test-runners).
|
||||
|
||||
#### Linters (flake8, black and mypy)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||

|
||||
[](https://pypistats.org/packages/optuna-dashboard)
|
||||
[](https://optuna-dashboard.readthedocs.io/en/latest/?badge=latest)
|
||||
[](https://codecov.io/gh/optuna/optuna-dashboard)
|
||||
|
||||
|
||||
Real-time dashboard for [Optuna](https://github.com/optuna/optuna).
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import re
|
||||
|
||||
import optuna
|
||||
from optuna.trial import TrialState
|
||||
from optuna_dashboard import ChoiceWidget
|
||||
from optuna_dashboard import register_objective_form_widgets
|
||||
from playwright.sync_api import expect
|
||||
from playwright.sync_api import Page
|
||||
import pytest
|
||||
|
||||
from ...test_server import make_test_server
|
||||
|
||||
|
||||
def make_test_storage() -> optuna.storages.InMemoryStorage:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
sampler = optuna.samplers.RandomSampler(seed=0)
|
||||
|
||||
study = optuna.create_study(
|
||||
study_name="preferential_optimization",
|
||||
storage=storage,
|
||||
sampler=sampler,
|
||||
)
|
||||
|
||||
register_objective_form_widgets(
|
||||
study,
|
||||
widgets=[
|
||||
ChoiceWidget(
|
||||
choices=["Good", "So-so", "Bad"],
|
||||
values=[-1, 0, 1],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
n_batch = 4
|
||||
while True:
|
||||
running_trials = study.get_trials(deepcopy=False, states=(TrialState.RUNNING,))
|
||||
if len(running_trials) >= n_batch:
|
||||
break
|
||||
study.ask()
|
||||
|
||||
return storage
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def storage() -> optuna.storages.InMemoryStorage:
|
||||
storage = make_test_storage()
|
||||
return storage
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def server_url(request: pytest.FixtureRequest, storage: optuna.storages.InMemoryStorage) -> str:
|
||||
return make_test_server(request, storage)
|
||||
|
||||
|
||||
def test_preferential_optimization(
|
||||
page: Page,
|
||||
storage: optuna.storages.InMemoryStorage,
|
||||
server_url: str,
|
||||
) -> None:
|
||||
summaries = optuna.get_all_study_summaries(storage)
|
||||
study_id = summaries[0]._study_id
|
||||
url = f"{server_url}/studies/{study_id}/trials"
|
||||
|
||||
page.goto(url)
|
||||
|
||||
# Confirm that the trial list page is displayed.
|
||||
expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text(
|
||||
"Trial 0 (trial_id=0)"
|
||||
)
|
||||
page.get_by_label("Filter").click()
|
||||
# Confirm that all trials are running.
|
||||
expect(
|
||||
page.get_by_text("Complete (0)Pruned (0)Fail (0)Running (4)Waiting (0)")
|
||||
).to_be_visible()
|
||||
page.locator(".MuiBackdrop-root").click()
|
||||
|
||||
# Confirm that the trial detail page is displayed.
|
||||
expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text(
|
||||
"Trial 0 (trial_id=0)"
|
||||
)
|
||||
# This trial is running.
|
||||
expect(page.get_by_text("Running", exact=True).nth(4)).to_be_visible()
|
||||
page.get_by_label("Bad").check()
|
||||
page.get_by_role("button", name="Submit").click()
|
||||
# This trial is completed and is the best trial.
|
||||
expect(page.get_by_text("Complete").nth(1)).to_be_visible()
|
||||
expect(page.get_by_text("Best Trial").nth(1)).to_be_visible()
|
||||
|
||||
# Move the next trial page.
|
||||
page.get_by_role("button", name="Trial 1 Running").click()
|
||||
# Confirm that the trial detail page is displayed.
|
||||
expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text(
|
||||
"Trial 1 (trial_id=1)"
|
||||
)
|
||||
# This trial is running.
|
||||
expect(page.get_by_text("Running", exact=True).nth(3)).to_be_visible()
|
||||
page.get_by_label("So-so").check()
|
||||
page.get_by_role("button", name="Submit").click()
|
||||
# This trial is completed and is the best trial.
|
||||
expect(page.get_by_text("Complete").nth(2)).to_be_visible()
|
||||
expect(page.get_by_text("Best Trial").nth(1)).to_be_visible()
|
||||
|
||||
# Move the next trial page.
|
||||
page.get_by_role("button", name="Trial 2 Running").click()
|
||||
# Confirm that the trial detail page is displayed.
|
||||
expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text(
|
||||
"Trial 2 (trial_id=2)"
|
||||
)
|
||||
# This trial is running.
|
||||
expect(page.get_by_text("Running", exact=True).nth(2)).to_be_visible()
|
||||
page.get_by_label("Good").check()
|
||||
page.get_by_role("button", name="Submit").click()
|
||||
# This trial is completed and is the best trial.
|
||||
expect(page.get_by_text("Complete").nth(3)).to_be_visible()
|
||||
expect(page.get_by_text("Best Trial").nth(1)).to_be_visible()
|
||||
|
||||
# Move the next trial page.
|
||||
page.get_by_role("button", name="Trial 3 Running").click()
|
||||
# Confirm that the trial detail page is displayed.
|
||||
expect(page.get_by_role("heading").filter(has_text=re.compile("Trial"))).to_contain_text(
|
||||
"Trial 3 (trial_id=3)"
|
||||
)
|
||||
# This trial is running.
|
||||
expect(page.get_by_text("Running", exact=True).nth(1)).to_be_visible()
|
||||
page.get_by_role("button", name="Fail Trial").click()
|
||||
# This trial is failed.
|
||||
expect(page.get_by_text("Fail").nth(1)).to_be_visible()
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import numbers
|
||||
import threading
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
@@ -85,9 +86,8 @@ class _CachedExtraStudyProperty:
|
||||
self._cursor = next_cursor
|
||||
|
||||
def _update_user_attrs(self, trial: FrozenTrial) -> None:
|
||||
# TODO(c-bata): Support numpy-specific number types.
|
||||
current_user_attrs = {
|
||||
k: not isinstance(v, bool) and isinstance(v, (int, float))
|
||||
k: not isinstance(v, bool) and isinstance(v, numbers.Real)
|
||||
for k, v in trial.user_attrs.items()
|
||||
}
|
||||
for attr_name, current_is_sortable in current_user_attrs.items():
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
import numbers
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
@@ -104,6 +105,8 @@ def serialize_attrs(attrs: dict[str, Any]) -> list[Attribute]:
|
||||
value = "<binary object>"
|
||||
elif isinstance(v, str):
|
||||
value = v
|
||||
elif isinstance(v, numbers.Real):
|
||||
value = str(v)
|
||||
else:
|
||||
value = json.dumps(v)
|
||||
value = value[:MAX_ATTR_LENGTH] if len(value) > MAX_ATTR_LENGTH else value
|
||||
|
||||
@@ -105,7 +105,7 @@ def register_artifact_route(
|
||||
|
||||
@app.post("/api/artifacts/<study_id:int>/<trial_id:int>")
|
||||
@json_api_view
|
||||
def upload_artifact_api(study_id: int, trial_id: int) -> dict[str, Any]:
|
||||
def upload_trial_artifact_api(study_id: int, trial_id: int) -> dict[str, Any]:
|
||||
trial = storage.get_trial(trial_id)
|
||||
if trial is None:
|
||||
response.status = 400
|
||||
@@ -144,9 +144,42 @@ def register_artifact_route(
|
||||
"artifacts": list_trial_artifacts(storage.get_study_system_attrs(study_id), trial),
|
||||
}
|
||||
|
||||
@app.post("/api/artifacts/<study_id:int>")
|
||||
@json_api_view
|
||||
def upload_study_artifact_api(study_id: int) -> dict[str, Any]:
|
||||
if artifact_store is None:
|
||||
response.status = 400 # Bad Request
|
||||
return {"reason": "Cannot access to the artifacts."}
|
||||
file = request.json.get("file")
|
||||
if file is None:
|
||||
response.status = 400
|
||||
return {"reason": "Please specify the 'file' key."}
|
||||
|
||||
_, data = parse_data_uri(file)
|
||||
filename = request.json.get("filename", "")
|
||||
artifact_id = str(uuid.uuid4())
|
||||
artifact_store.write(artifact_id, io.BytesIO(data))
|
||||
|
||||
mimetype, encoding = mimetypes.guess_type(filename)
|
||||
artifact = {
|
||||
"artifact_id": artifact_id,
|
||||
"filename": filename,
|
||||
"mimetype": mimetype or DEFAULT_MIME_TYPE,
|
||||
"encoding": encoding,
|
||||
}
|
||||
attr_key = ARTIFACTS_ATTR_PREFIX + artifact_id
|
||||
storage.set_study_system_attr(study_id, attr_key, json.dumps(artifact))
|
||||
|
||||
response.status = 201
|
||||
|
||||
return {
|
||||
"artifact_id": artifact_id,
|
||||
"artifacts": list_study_artifacts(storage.get_study_system_attrs(study_id)),
|
||||
}
|
||||
|
||||
@app.delete("/api/artifacts/<study_id:int>/<trial_id:int>/<artifact_id:re:[0-9a-fA-F-]+>")
|
||||
@json_api_view
|
||||
def delete_artifact(study_id: int, trial_id: int, artifact_id: str) -> dict[str, Any]:
|
||||
def delete_trial_artifact(study_id: int, trial_id: int, artifact_id: str) -> dict[str, Any]:
|
||||
if artifact_store is None:
|
||||
response.status = 400 # Bad Request
|
||||
return {"reason": "Cannot access to the artifacts."}
|
||||
@@ -154,7 +187,7 @@ def register_artifact_route(
|
||||
|
||||
# The artifact's metadata is stored in one of the following two locations:
|
||||
storage.set_study_system_attr(
|
||||
study_id, _dashboard_trial_artifact_prefix(trial_id) + artifact_id, json.dumps(None)
|
||||
study_id, _dashboard_artifact_prefix(trial_id) + artifact_id, json.dumps(None)
|
||||
)
|
||||
storage.set_trial_system_attr(
|
||||
trial_id, ARTIFACTS_ATTR_PREFIX + artifact_id, json.dumps(None)
|
||||
@@ -163,6 +196,21 @@ def register_artifact_route(
|
||||
response.status = 204
|
||||
return {}
|
||||
|
||||
@app.delete("/api/artifacts/<study_id:int>/<artifact_id:re:[0-9a-fA-F-]+>")
|
||||
@json_api_view
|
||||
def delete_study_artifact(study_id: int, artifact_id: str) -> dict[str, Any]:
|
||||
if artifact_store is None:
|
||||
response.status = 400 # Bad Request
|
||||
return {"reason": "Cannot access to the artifacts."}
|
||||
artifact_store.remove(artifact_id)
|
||||
|
||||
storage.set_study_system_attr(
|
||||
study_id, ARTIFACTS_ATTR_PREFIX + artifact_id, json.dumps(None)
|
||||
)
|
||||
|
||||
response.status = 204
|
||||
return {}
|
||||
|
||||
|
||||
def upload_artifact(
|
||||
backend: ArtifactBackend,
|
||||
@@ -220,7 +268,7 @@ def upload_artifact(
|
||||
return artifact_id
|
||||
|
||||
|
||||
def _dashboard_trial_artifact_prefix(trial_id: int) -> str:
|
||||
def _dashboard_artifact_prefix(trial_id: int) -> str:
|
||||
return DASHBOARD_ARTIFACTS_ATTR_PREFIX + f"{trial_id}:"
|
||||
|
||||
|
||||
@@ -240,7 +288,7 @@ def get_trial_artifact_meta(
|
||||
) -> Optional[ArtifactMeta]:
|
||||
# Search study_system_attrs due to backward compatibility.
|
||||
study_system_attrs = storage.get_study_system_attrs(study_id)
|
||||
attr_key = _dashboard_trial_artifact_prefix(trial_id=trial_id) + artifact_id
|
||||
attr_key = _dashboard_artifact_prefix(trial_id=trial_id) + artifact_id
|
||||
artifact_meta = study_system_attrs.get(attr_key)
|
||||
if artifact_meta is not None:
|
||||
return json.loads(artifact_meta)
|
||||
@@ -284,7 +332,7 @@ def list_trial_artifacts(
|
||||
dashboard_artifact_metas = [
|
||||
json.loads(value)
|
||||
for key, value in study_system_attrs.items()
|
||||
if key.startswith(_dashboard_trial_artifact_prefix(trial._trial_id))
|
||||
if key.startswith(_dashboard_artifact_prefix(trial._trial_id))
|
||||
]
|
||||
|
||||
# Collect ArtifactMeta from trial_system_attrs. Note that artifacts uploaded via
|
||||
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
tellTrialAPI,
|
||||
saveTrialUserAttrsAPI,
|
||||
renameStudyAPI,
|
||||
uploadArtifactAPI,
|
||||
uploadTrialArtifactAPI,
|
||||
uploadStudyArtifactAPI,
|
||||
getMetaInfoAPI,
|
||||
deleteArtifactAPI,
|
||||
deleteTrialArtifactAPI,
|
||||
deleteStudyArtifactAPI,
|
||||
reportPreferenceAPI,
|
||||
skipPreferentialTrialAPI,
|
||||
removePreferentialHistoryAPI,
|
||||
@@ -100,7 +102,13 @@ export const actionCreator = () => {
|
||||
setTrial(studyId, trialIndex, newTrial)
|
||||
}
|
||||
|
||||
const deleteTrialArtifact = (
|
||||
const setStudyArtifacts = (studyId: number, artifacts: Artifact[]) => {
|
||||
const newStudy: StudyDetail = Object.assign({}, studyDetails[studyId])
|
||||
newStudy.artifacts = artifacts
|
||||
setStudyDetailState(studyId, newStudy)
|
||||
}
|
||||
|
||||
const deleteTrialArtifactState = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
artifact_id: string
|
||||
@@ -122,6 +130,18 @@ export const actionCreator = () => {
|
||||
setTrialArtifacts(studyId, index, newArtifacts)
|
||||
}
|
||||
|
||||
const deleteStudyArtifactState = (studyId: number, artifact_id: string) => {
|
||||
const artifacts = studyDetails[studyId].artifacts
|
||||
const artifactIndex = artifacts.findIndex(
|
||||
(a) => a.artifact_id === artifact_id
|
||||
)
|
||||
const newArtifacts = [
|
||||
...artifacts.slice(0, artifactIndex),
|
||||
...artifacts.slice(artifactIndex + 1, artifacts.length),
|
||||
]
|
||||
setStudyArtifacts(studyId, newArtifacts)
|
||||
}
|
||||
|
||||
const setTrialStateValues = (
|
||||
studyId: number,
|
||||
index: number,
|
||||
@@ -154,7 +174,7 @@ export const actionCreator = () => {
|
||||
currentValue > bestValue
|
||||
) {
|
||||
newStudy.best_trials = [newTrial]
|
||||
} else if (currentValue == bestValue) {
|
||||
} else if (currentValue === bestValue) {
|
||||
newStudy.best_trials = [...newStudy.best_trials, newTrial]
|
||||
}
|
||||
}
|
||||
@@ -430,7 +450,7 @@ export const actionCreator = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const uploadArtifact = (
|
||||
const uploadTrialArtifact = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
file: File
|
||||
@@ -439,7 +459,7 @@ export const actionCreator = () => {
|
||||
setUploading(true)
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = (upload: ProgressEvent<FileReader>) => {
|
||||
uploadArtifactAPI(
|
||||
uploadTrialArtifactAPI(
|
||||
studyId,
|
||||
trialId,
|
||||
file.name,
|
||||
@@ -467,14 +487,56 @@ export const actionCreator = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteArtifact = (
|
||||
const uploadStudyArtifact = (studyId: number, file: File): void => {
|
||||
const reader = new FileReader()
|
||||
setUploading(true)
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = (upload: ProgressEvent<FileReader>) => {
|
||||
uploadStudyArtifactAPI(
|
||||
studyId,
|
||||
file.name,
|
||||
upload.target?.result as string
|
||||
)
|
||||
.then((res) => {
|
||||
setUploading(false)
|
||||
setStudyArtifacts(studyId, res.artifacts)
|
||||
})
|
||||
.catch((err) => {
|
||||
setUploading(false)
|
||||
const reason = err.response?.data.reason
|
||||
enqueueSnackbar(`Failed to upload ${reason}`, { variant: "error" })
|
||||
})
|
||||
}
|
||||
reader.onerror = (error) => {
|
||||
enqueueSnackbar(`Failed to read the file ${error}`, { variant: "error" })
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteTrialArtifact = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
artifactId: string
|
||||
): void => {
|
||||
deleteArtifactAPI(studyId, trialId, artifactId)
|
||||
deleteTrialArtifactAPI(studyId, trialId, artifactId)
|
||||
.then(() => {
|
||||
deleteTrialArtifact(studyId, trialId, artifactId)
|
||||
deleteTrialArtifactState(studyId, trialId, artifactId)
|
||||
enqueueSnackbar(`Success to delete an artifact.`, {
|
||||
variant: "success",
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
const reason = err.response?.data.reason
|
||||
enqueueSnackbar(`Failed to delete ${reason}.`, {
|
||||
variant: "error",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const deleteStudyArtifact = (studyId: number, artifactId: string): void => {
|
||||
deleteStudyArtifactAPI(studyId, artifactId)
|
||||
.then(() => {
|
||||
deleteStudyArtifactState(studyId, artifactId)
|
||||
enqueueSnackbar(`Success to delete an artifact.`, {
|
||||
variant: "success",
|
||||
})
|
||||
@@ -693,8 +755,10 @@ export const actionCreator = () => {
|
||||
saveReloadInterval,
|
||||
saveStudyNote,
|
||||
saveTrialNote,
|
||||
uploadArtifact,
|
||||
deleteArtifact,
|
||||
uploadTrialArtifact,
|
||||
uploadStudyArtifact,
|
||||
deleteTrialArtifact,
|
||||
deleteStudyArtifact,
|
||||
makeTrialComplete,
|
||||
makeTrialFail,
|
||||
saveTrialUserAttrs,
|
||||
|
||||
@@ -280,7 +280,7 @@ type UploadArtifactAPIResponse = {
|
||||
artifacts: Artifact[]
|
||||
}
|
||||
|
||||
export const uploadArtifactAPI = (
|
||||
export const uploadTrialArtifactAPI = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
fileName: string,
|
||||
@@ -296,7 +296,22 @@ export const uploadArtifactAPI = (
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteArtifactAPI = (
|
||||
export const uploadStudyArtifactAPI = (
|
||||
studyId: number,
|
||||
fileName: string,
|
||||
dataUrl: string
|
||||
): Promise<UploadArtifactAPIResponse> => {
|
||||
return axiosInstance
|
||||
.post<UploadArtifactAPIResponse>(`/api/artifacts/${studyId}`, {
|
||||
file: dataUrl,
|
||||
filename: fileName,
|
||||
})
|
||||
.then((res) => {
|
||||
return res.data
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteTrialArtifactAPI = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
artifactId: string
|
||||
@@ -308,6 +323,17 @@ export const deleteArtifactAPI = (
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteStudyArtifactAPI = (
|
||||
studyId: number,
|
||||
artifactId: string
|
||||
): Promise<void> => {
|
||||
return axiosInstance
|
||||
.delete<void>(`/api/artifacts/${studyId}/${artifactId}`)
|
||||
.then(() => {
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
export const tellTrialAPI = (
|
||||
trialId: number,
|
||||
state: TrialStateFinished,
|
||||
|
||||
@@ -384,7 +384,7 @@ function stableSort<T>(
|
||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number])
|
||||
stabilizedThis.sort((a, b) => {
|
||||
if (less) {
|
||||
const ascending = order == "asc"
|
||||
const ascending = order === "asc"
|
||||
const result = ascending
|
||||
? -less(a[0], b[0], ascending)
|
||||
: less(a[0], b[0], ascending)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactNode, useState } from "react"
|
||||
import React, { ReactNode, useState, FC } from "react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@mui/material"
|
||||
import { actionCreator } from "../action"
|
||||
|
||||
export const useDeleteArtifactDialog = (): [
|
||||
export const useDeleteTrialArtifactDialog = (): [
|
||||
(studyId: number, trialId: number, artifact: Artifact) => void,
|
||||
() => ReactNode
|
||||
] => {
|
||||
@@ -33,7 +33,7 @@ export const useDeleteArtifactDialog = (): [
|
||||
if (artifact === null) {
|
||||
return
|
||||
}
|
||||
action.deleteArtifact(studyId, trialId, artifact.artifact_id)
|
||||
action.deleteTrialArtifact(studyId, trialId, artifact.artifact_id)
|
||||
setOpenDeleteArtifactDialog(false)
|
||||
setTarget([-1, -1, null])
|
||||
}
|
||||
@@ -45,32 +45,96 @@ export const useDeleteArtifactDialog = (): [
|
||||
|
||||
const renderDeleteArtifactDialog = () => {
|
||||
return (
|
||||
<Dialog
|
||||
open={openDeleteArtifactDialog}
|
||||
onClose={() => {
|
||||
handleCloseDeleteArtifactDialog()
|
||||
}}
|
||||
aria-labelledby="delete-artifact-dialog-title"
|
||||
>
|
||||
<DialogTitle id="delete-artifact-dialog-title">
|
||||
Delete artifact
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Are you sure you want to delete an artifact ("
|
||||
{target[2]?.filename}")?
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDeleteArtifactDialog} color="primary">
|
||||
No
|
||||
</Button>
|
||||
<Button onClick={handleDeleteArtifact} color="primary">
|
||||
Yes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<DeleteDialog
|
||||
openDeleteArtifactDialog={openDeleteArtifactDialog}
|
||||
handleCloseDeleteArtifactDialog={handleCloseDeleteArtifactDialog}
|
||||
filename={target[2]?.filename}
|
||||
handleDeleteArtifact={handleDeleteArtifact}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return [openDialog, renderDeleteArtifactDialog]
|
||||
}
|
||||
|
||||
export const useDeleteStudyArtifactDialog = (): [
|
||||
(studyId: number, artifact: Artifact) => void,
|
||||
() => ReactNode
|
||||
] => {
|
||||
const action = actionCreator()
|
||||
|
||||
const [openDeleteArtifactDialog, setOpenDeleteArtifactDialog] =
|
||||
useState(false)
|
||||
const [target, setTarget] = useState<[number, Artifact | null]>([-1, null])
|
||||
|
||||
const handleCloseDeleteArtifactDialog = () => {
|
||||
setOpenDeleteArtifactDialog(false)
|
||||
setTarget([-1, null])
|
||||
}
|
||||
|
||||
const handleDeleteArtifact = () => {
|
||||
const [studyId, artifact] = target
|
||||
if (artifact === null) {
|
||||
return
|
||||
}
|
||||
action.deleteStudyArtifact(studyId, artifact.artifact_id)
|
||||
setOpenDeleteArtifactDialog(false)
|
||||
setTarget([-1, null])
|
||||
}
|
||||
|
||||
const openDialog = (studyId: number, artifact: Artifact) => {
|
||||
setTarget([studyId, artifact])
|
||||
setOpenDeleteArtifactDialog(true)
|
||||
}
|
||||
|
||||
const renderDeleteArtifactDialog = () => {
|
||||
return (
|
||||
<DeleteDialog
|
||||
openDeleteArtifactDialog={openDeleteArtifactDialog}
|
||||
handleCloseDeleteArtifactDialog={handleCloseDeleteArtifactDialog}
|
||||
filename={target[1]?.filename}
|
||||
handleDeleteArtifact={handleDeleteArtifact}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return [openDialog, renderDeleteArtifactDialog]
|
||||
}
|
||||
|
||||
const DeleteDialog: FC<{
|
||||
openDeleteArtifactDialog: boolean
|
||||
handleCloseDeleteArtifactDialog: () => void
|
||||
filename: string | undefined
|
||||
handleDeleteArtifact: () => void
|
||||
}> = ({
|
||||
openDeleteArtifactDialog,
|
||||
handleCloseDeleteArtifactDialog,
|
||||
filename,
|
||||
handleDeleteArtifact,
|
||||
}) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={openDeleteArtifactDialog}
|
||||
onClose={() => {
|
||||
handleCloseDeleteArtifactDialog()
|
||||
}}
|
||||
aria-labelledby="delete-artifact-dialog-title"
|
||||
>
|
||||
<DialogTitle id="delete-artifact-dialog-title">
|
||||
Delete artifact
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Are you sure you want to delete an artifact ("
|
||||
{filename}")?
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDeleteArtifactDialog} color="primary">
|
||||
No
|
||||
</Button>
|
||||
<Button onClick={handleDeleteArtifact} color="primary">
|
||||
Yes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ const plotIntermediateValue = (
|
||||
t.state === "Pruned" &&
|
||||
t.values &&
|
||||
t.values.length > 0) ||
|
||||
t.state == "Running"
|
||||
t.state === "Running"
|
||||
)
|
||||
const plotData: Partial<plotly.PlotData>[] = filteredTrials.map((trial) => {
|
||||
const values = trial.intermediate_values.filter(
|
||||
|
||||
@@ -164,7 +164,7 @@ const plotCoordinate = (
|
||||
return truncated
|
||||
.split("")
|
||||
.map((c, i) => {
|
||||
return (i + 1) % breakLength == 0 ? c + "<br>" : c
|
||||
return (i + 1) % breakLength === 0 ? c + "<br>" : c
|
||||
})
|
||||
.join("")
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ const ArtifactUploader: FC<{
|
||||
if (files === null) {
|
||||
return
|
||||
}
|
||||
action.uploadArtifact(studyId, trialId, files[0])
|
||||
action.uploadTrialArtifact(studyId, trialId, files[0])
|
||||
}
|
||||
|
||||
const handleDrop: DragEventHandler = (e) => {
|
||||
@@ -433,7 +433,7 @@ const ArtifactUploader: FC<{
|
||||
e.preventDefault()
|
||||
const file = e.dataTransfer.files[0]
|
||||
setDragOver(false)
|
||||
action.uploadArtifact(studyId, trialId, file)
|
||||
action.uploadTrialArtifact(studyId, trialId, file)
|
||||
}
|
||||
|
||||
const handleDragOver: DragEventHandler = (e) => {
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import React, {
|
||||
ChangeEventHandler,
|
||||
DragEventHandler,
|
||||
FC,
|
||||
MouseEventHandler,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
useTheme,
|
||||
IconButton,
|
||||
Card,
|
||||
CardContent,
|
||||
CardActionArea,
|
||||
} from "@mui/material"
|
||||
import UploadFileIcon from "@mui/icons-material/UploadFile"
|
||||
import DownloadIcon from "@mui/icons-material/Download"
|
||||
import DeleteIcon from "@mui/icons-material/Delete"
|
||||
import FullscreenIcon from "@mui/icons-material/Fullscreen"
|
||||
|
||||
import { actionCreator } from "../action"
|
||||
import { useDeleteStudyArtifactDialog } from "./DeleteArtifactDialog"
|
||||
import {
|
||||
useThreejsArtifactModal,
|
||||
isThreejsArtifact,
|
||||
} from "./ThreejsArtifactViewer"
|
||||
import { ArtifactCardMedia } from "./ArtifactCardMedia"
|
||||
|
||||
export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => {
|
||||
const theme = useTheme()
|
||||
const [openDeleteArtifactDialog, renderDeleteArtifactDialog] =
|
||||
useDeleteStudyArtifactDialog()
|
||||
const [openThreejsArtifactModal, renderThreejsArtifactModal] =
|
||||
useThreejsArtifactModal()
|
||||
|
||||
const width = "200px"
|
||||
const height = "150px"
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", p: theme.spacing(1, 0) }}>
|
||||
{study.artifacts.map((artifact) => {
|
||||
const urlPath = `/artifacts/${study.id}/${artifact.artifact_id}`
|
||||
return (
|
||||
<Card
|
||||
key={artifact.artifact_id}
|
||||
sx={{
|
||||
marginBottom: theme.spacing(2),
|
||||
width: width,
|
||||
margin: theme.spacing(0, 1, 1, 0),
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<ArtifactCardMedia
|
||||
artifact={artifact}
|
||||
urlPath={urlPath}
|
||||
height={height}
|
||||
/>
|
||||
<CardContent
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
padding: `${theme.spacing(1)} !important`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
p: theme.spacing(0.5, 0),
|
||||
flexGrow: 1,
|
||||
wordWrap: "break-word",
|
||||
maxWidth: `calc(100% - ${
|
||||
isThreejsArtifact(artifact)
|
||||
? theme.spacing(12)
|
||||
: theme.spacing(8)
|
||||
})`,
|
||||
}}
|
||||
>
|
||||
{artifact.filename}
|
||||
</Typography>
|
||||
{isThreejsArtifact(artifact) ? (
|
||||
<IconButton
|
||||
aria-label="show artifact 3d model"
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ margin: "auto 0" }}
|
||||
onClick={() => {
|
||||
openThreejsArtifactModal(urlPath, artifact)
|
||||
}}
|
||||
>
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
<IconButton
|
||||
aria-label="delete artifact"
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ margin: "auto 0" }}
|
||||
onClick={() => {
|
||||
openDeleteArtifactDialog(study.id, artifact)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="download artifact"
|
||||
size="small"
|
||||
color="inherit"
|
||||
download={artifact.filename}
|
||||
sx={{ margin: "auto 0" }}
|
||||
href={urlPath}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
<StudyArtifactUploader study={study} width={width} height={height} />
|
||||
</Box>
|
||||
{renderDeleteArtifactDialog()}
|
||||
{renderThreejsArtifactModal()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const StudyArtifactUploader: FC<{
|
||||
study: StudyDetail
|
||||
width: string
|
||||
height: string
|
||||
}> = ({ study, width, height }) => {
|
||||
const theme = useTheme()
|
||||
const [dragOver, setDragOver] = useState<boolean>(false)
|
||||
const action = actionCreator()
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const handleClick: MouseEventHandler = () => {
|
||||
if (!inputRef || !inputRef.current) {
|
||||
return
|
||||
}
|
||||
inputRef.current.click()
|
||||
}
|
||||
|
||||
const handleOnChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
const files = e.target.files
|
||||
if (files === null) {
|
||||
return
|
||||
}
|
||||
action.uploadStudyArtifact(study.id, files[0])
|
||||
}
|
||||
|
||||
const handleDragOver: DragEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = "copy"
|
||||
setDragOver(true)
|
||||
}
|
||||
|
||||
const handleDragLeave: DragEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = "copy"
|
||||
setDragOver(false)
|
||||
}
|
||||
|
||||
const handleDrop: DragEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
const files = e.dataTransfer.files
|
||||
setDragOver(false)
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
action.uploadStudyArtifact(study.id, files[i])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
marginBottom: theme.spacing(2),
|
||||
width: width,
|
||||
minHeight: height,
|
||||
margin: theme.spacing(0, 1, 1, 0),
|
||||
border: dragOver
|
||||
? `3px dashed ${theme.palette.mode === "dark" ? "white" : "black"}`
|
||||
: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<CardActionArea
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<UploadFileIcon
|
||||
sx={{ fontSize: 80, marginBottom: theme.spacing(2) }}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
ref={inputRef}
|
||||
onChange={handleOnChange}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<Typography>Upload a New File</Typography>
|
||||
<Typography
|
||||
sx={{ textAlign: "center", color: theme.palette.grey.A400 }}
|
||||
>
|
||||
Drag your file here or click to browse.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -145,6 +145,8 @@ export const StudyDetail: FC<{
|
||||
</Grid2>
|
||||
</Box>
|
||||
)
|
||||
} else if (page === "trialList") {
|
||||
content = <TrialList studyDetail={studyDetail} />
|
||||
} else if (page === "trialTable") {
|
||||
content = (
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
@@ -153,8 +155,6 @@ export const StudyDetail: FC<{
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
} else if (page === "trialList") {
|
||||
content = <TrialList studyDetail={studyDetail} />
|
||||
} else if (page === "note" && studyDetail !== null) {
|
||||
content = (
|
||||
<Box
|
||||
@@ -192,7 +192,7 @@ export const StudyDetail: FC<{
|
||||
<PreferentialGraph studyDetail={studyDetail} />
|
||||
</Box>
|
||||
)
|
||||
} else if (page == "preferenceHistory") {
|
||||
} else if (page === "preferenceHistory") {
|
||||
content = <PreferenceHistory studyDetail={studyDetail} />
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,15 @@ import { DataGrid, DataGridColumn } from "./DataGrid"
|
||||
import { GraphHyperparameterImportance } from "./GraphHyperparameterImportances"
|
||||
import { UserDefinedPlot } from "./UserDefinedPlot"
|
||||
import { BestTrialsCard } from "./BestTrialsCard"
|
||||
import { StudyArtifactCards } from "./StudyArtifactCards"
|
||||
import { useRecoilValue } from "recoil"
|
||||
import {
|
||||
useStudyDetailValue,
|
||||
useStudyDirections,
|
||||
useStudySummaryValue,
|
||||
} from "../state"
|
||||
import FormControlLabel from "@mui/material/FormControlLabel"
|
||||
import { artifactIsAvailable } from "../state"
|
||||
|
||||
export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
const theme = useTheme()
|
||||
@@ -31,6 +34,7 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
const studyDetail = useStudyDetailValue(studyId)
|
||||
const [logScale, setLogScale] = useState<boolean>(false)
|
||||
const [includePruned, setIncludePruned] = useState<boolean>(true)
|
||||
const artifactEnabled = useRecoilValue<boolean>(artifactIsAvailable)
|
||||
|
||||
const handleLogScaleChange = () => {
|
||||
setLogScale(!logScale)
|
||||
@@ -105,7 +109,7 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
</Card>
|
||||
<Grid2 container spacing={2} sx={{ padding: theme.spacing(0, 2) }}>
|
||||
{studyDetail !== null &&
|
||||
studyDetail.directions.length == 1 &&
|
||||
studyDetail.directions.length === 1 &&
|
||||
studyDetail.has_intermediate_values ? (
|
||||
<Grid2 xs={6}>
|
||||
<GraphIntermediateValues
|
||||
@@ -167,6 +171,32 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
</Card>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
|
||||
{artifactEnabled && studyDetail !== null && (
|
||||
<Grid2 container spacing={2} sx={{ padding: theme.spacing(0, 2) }}>
|
||||
<Grid2 xs={6}>
|
||||
<Card>
|
||||
<CardContent
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
margin: "1em 0",
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
}}
|
||||
>
|
||||
Study Artifacts
|
||||
</Typography>
|
||||
<StudyArtifactCards study={studyDetail} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import DeleteIcon from "@mui/icons-material/Delete"
|
||||
import FullscreenIcon from "@mui/icons-material/Fullscreen"
|
||||
|
||||
import { actionCreator } from "../action"
|
||||
import { useDeleteArtifactDialog } from "./DeleteArtifactDialog"
|
||||
import { useDeleteTrialArtifactDialog } from "./DeleteArtifactDialog"
|
||||
import {
|
||||
useThreejsArtifactModal,
|
||||
isThreejsArtifact,
|
||||
@@ -31,7 +31,7 @@ import { ArtifactCardMedia } from "./ArtifactCardMedia"
|
||||
export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => {
|
||||
const theme = useTheme()
|
||||
const [openDeleteArtifactDialog, renderDeleteArtifactDialog] =
|
||||
useDeleteArtifactDialog()
|
||||
useDeleteTrialArtifactDialog()
|
||||
const [openThreejsArtifactModal, renderThreejsArtifactModal] =
|
||||
useThreejsArtifactModal()
|
||||
|
||||
@@ -158,7 +158,7 @@ const TrialArtifactUploader: FC<{
|
||||
if (files === null) {
|
||||
return
|
||||
}
|
||||
action.uploadArtifact(trial.study_id, trial.trial_id, files[0])
|
||||
action.uploadTrialArtifact(trial.study_id, trial.trial_id, files[0])
|
||||
}
|
||||
const handleDrop: DragEventHandler = (e) => {
|
||||
e.stopPropagation()
|
||||
@@ -166,7 +166,7 @@ const TrialArtifactUploader: FC<{
|
||||
const files = e.dataTransfer.files
|
||||
setDragOver(false)
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
action.uploadArtifact(trial.study_id, trial.trial_id, files[i])
|
||||
action.uploadTrialArtifact(trial.study_id, trial.trial_id, files[i])
|
||||
}
|
||||
}
|
||||
const handleDragOver: DragEventHandler = (e) => {
|
||||
|
||||
@@ -45,12 +45,12 @@ export const TrialFormWidgets: FC<{
|
||||
? "Set Objective Values Form"
|
||||
: "Set Objective Value Form"
|
||||
const widgetNames = formWidgets.widgets.map((widget, i) => {
|
||||
if (formWidgets.output_type == "objective") {
|
||||
if (formWidgets.output_type === "objective") {
|
||||
if (objectiveNames.at(i) !== undefined) {
|
||||
return objectiveNames[i]
|
||||
}
|
||||
return directions.length == 1 ? "Objective" : `Objective ${i}`
|
||||
} else if (formWidgets.output_type == "user_attr") {
|
||||
return directions.length === 1 ? "Objective" : `Objective ${i}`
|
||||
} else if (formWidgets.output_type === "user_attr") {
|
||||
if (widget.type !== "user_attr" && widget.user_attr_key !== undefined) {
|
||||
return widget.user_attr_key
|
||||
}
|
||||
@@ -118,13 +118,13 @@ const UpdatableFormWidgets: FC<{
|
||||
const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
e.preventDefault()
|
||||
const values = widgetStates.map((ws) => ws.value)
|
||||
if (formWidgets.output_type == "objective") {
|
||||
if (formWidgets.output_type === "objective") {
|
||||
const filtered = values.filter<number>((v): v is number => v !== null)
|
||||
if (filtered.length !== formWidgets.widgets.length) {
|
||||
return
|
||||
}
|
||||
action.makeTrialComplete(trial.study_id, trial.trial_id, filtered)
|
||||
} else if (formWidgets.output_type == "user_attr") {
|
||||
} else if (formWidgets.output_type === "user_attr") {
|
||||
const user_attrs = Object.fromEntries(
|
||||
formWidgets.widgets.map((widget, i) => [
|
||||
widget.user_attr_key,
|
||||
@@ -433,7 +433,7 @@ const ReadonlyFormWidgets: FC<{
|
||||
max={widget.max}
|
||||
step={widget.step}
|
||||
marks={
|
||||
widget.labels === null || widget.labels.length == 0
|
||||
widget.labels === null || widget.labels.length === 0
|
||||
? true
|
||||
: widget.labels
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ const useIsBestTrial = (
|
||||
return useMemo(() => {
|
||||
const bestTrialIDs = studyDetail?.best_trials.map((t) => t.trial_id) || []
|
||||
return (trialId: number): boolean =>
|
||||
bestTrialIDs.findIndex((a) => a === trialId) != -1
|
||||
bestTrialIDs.findIndex((a) => a === trialId) !== -1
|
||||
}, [studyDetail])
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export const TrialTable: FC<{
|
||||
toCellValue: (i) => trials[i].state.toString(),
|
||||
},
|
||||
]
|
||||
if (studyDetail === null || studyDetail.directions.length == 1) {
|
||||
if (studyDetail === null || studyDetail.directions.length === 1) {
|
||||
columns.push({
|
||||
field: "values",
|
||||
label: "Value",
|
||||
|
||||
@@ -27,7 +27,7 @@ export const getDominatedTrials = (
|
||||
const dominatedTrials: boolean[] = []
|
||||
normalizedValues.forEach((values0: number[], i: number) => {
|
||||
const dominated = normalizedValues.some((values1: number[], j: number) => {
|
||||
if (i === j || values0.every((v, i) => v == values1[i])) {
|
||||
if (i === j || values0.every((v, i) => v === values1[i])) {
|
||||
return false
|
||||
}
|
||||
return values0.every((value0: number, k: number) => {
|
||||
|
||||
@@ -63,7 +63,7 @@ export const useStudyDetailValue = (studyId: number): StudyDetail | null => {
|
||||
|
||||
export const useStudySummaryValue = (studyId: number): StudySummary | null => {
|
||||
const studySummaries = useRecoilValue<StudySummary[]>(studySummariesState)
|
||||
return studySummaries.find((s) => s.study_id == studyId) || null
|
||||
return studySummaries.find((s) => s.study_id === studyId) || null
|
||||
}
|
||||
|
||||
export const useTrialUpdatingValue = (trialId: number): boolean => {
|
||||
|
||||
Generated
+812
-765
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -52,12 +52,12 @@
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react-syntax-highlighter": "^15.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.1",
|
||||
"@typescript-eslint/parser": "^4.26.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"esbuild-loader": "^2.18.0",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint": "^8.53.0",
|
||||
"jest": "^29.2.1",
|
||||
"jest-canvas-mock": "^2.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
|
||||
@@ -12,7 +12,7 @@ def test_get_artifact_path() -> None:
|
||||
|
||||
|
||||
def test_artifact_prefix() -> None:
|
||||
actual = _backend._dashboard_trial_artifact_prefix(trial_id=0)
|
||||
actual = _backend._dashboard_artifact_prefix(trial_id=0)
|
||||
assert actual == "dashboard:artifacts:0:"
|
||||
|
||||
|
||||
|
||||
@@ -220,6 +220,47 @@ class APITestCase(TestCase):
|
||||
assert study_detail["feedback_component_type"]["output_type"] == "artifact"
|
||||
assert study_detail["feedback_component_type"]["artifact_key"] == "image"
|
||||
|
||||
def test_save_trial_user_attrs(self) -> None:
|
||||
study = optuna.create_study()
|
||||
trials: list[optuna.Trial] = []
|
||||
for _ in range(2):
|
||||
trial = study.ask()
|
||||
trials.append(trial)
|
||||
|
||||
request_body = {
|
||||
"user_attrs": {
|
||||
"number": 0,
|
||||
},
|
||||
}
|
||||
|
||||
app = create_app(study._storage)
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trials[0]._trial_id}/user-attrs",
|
||||
"POST",
|
||||
content_type="application/json",
|
||||
body=json.dumps(request_body),
|
||||
)
|
||||
self.assertEqual(status, 204)
|
||||
|
||||
assert study.trials[0].user_attrs == request_body["user_attrs"]
|
||||
assert study.trials[1].user_attrs == {}
|
||||
|
||||
def test_save_trial_user_attrs_empty(self) -> None:
|
||||
study = optuna.create_study()
|
||||
trial = study.ask()
|
||||
|
||||
app = create_app(study._storage)
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial._trial_id}/user-attrs",
|
||||
"POST",
|
||||
content_type="application/json",
|
||||
body=json.dumps({}),
|
||||
)
|
||||
self.assertEqual(status, 400)
|
||||
assert study.trials[0].user_attrs == {}
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
|
||||
def test_skip_trial(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
@@ -399,6 +440,125 @@ class APITestCase(TestCase):
|
||||
)
|
||||
self.assertEqual(status, 404)
|
||||
|
||||
def test_tell_trial_complete(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
trial_id = study.ask()._trial_id
|
||||
|
||||
app = create_app(storage)
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial_id}/tell",
|
||||
"POST",
|
||||
body=json.dumps(
|
||||
{
|
||||
"state": "Complete",
|
||||
"values": [0, 1, 2],
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 204)
|
||||
trial = storage.get_trial(trial_id)
|
||||
assert trial.state == optuna.trial.TrialState.COMPLETE
|
||||
assert trial.values == [0, 1, 2]
|
||||
|
||||
def test_tell_trial_fail(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
trial_id = study.ask()._trial_id
|
||||
|
||||
app = create_app(storage)
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial_id}/tell",
|
||||
"POST",
|
||||
body=json.dumps(
|
||||
{
|
||||
"state": "Fail",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 204)
|
||||
trial = storage.get_trial(trial_id)
|
||||
assert trial.state == optuna.trial.TrialState.FAIL
|
||||
|
||||
def test_tell_trial_with_no_state(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
trial_id = study.ask()._trial_id
|
||||
|
||||
app = create_app(storage)
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial_id}/tell",
|
||||
"POST",
|
||||
body=json.dumps({}),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
def test_tell_trial_with_invalid_state(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
for state in ["Pruned", "Running", "Waiting", "Invalid"]:
|
||||
trial_id = study.ask()._trial_id
|
||||
app = create_app(storage)
|
||||
with self.subTest(state=state):
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial_id}/tell",
|
||||
"POST",
|
||||
body=json.dumps(
|
||||
{
|
||||
"state": state,
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
def test_tell_trial_with_no_values(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
trial_id = study.ask()._trial_id
|
||||
|
||||
app = create_app(storage)
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial_id}/tell",
|
||||
"POST",
|
||||
body=json.dumps(
|
||||
{
|
||||
"state": "Complete",
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
def test_tell_trial_with_invalid_values(self) -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
study = optuna.create_study(storage=storage)
|
||||
for values in [1.0, ["foo"]]:
|
||||
trial_id = study.ask()._trial_id
|
||||
app = create_app(storage)
|
||||
with self.subTest(values=values):
|
||||
status, _, _ = send_request(
|
||||
app,
|
||||
f"/api/trials/{trial_id}/tell",
|
||||
"POST",
|
||||
body=json.dumps(
|
||||
{
|
||||
"state": "Complete",
|
||||
"values": values,
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
|
||||
class BottleRequestHookTestCase(TestCase):
|
||||
def test_ignore_trailing_slashes(self) -> None:
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any
|
||||
from unittest import TestCase
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
import optuna
|
||||
from optuna import create_trial
|
||||
from optuna.distributions import BaseDistribution
|
||||
@@ -254,11 +255,29 @@ class _CachedExtraStudyPropertyUserAttrs(TestCase):
|
||||
|
||||
def test_infer_sortable(self) -> None:
|
||||
user_attrs_list: list[dict[str, Any]] = [
|
||||
{"a": 1, "b": 1, "c": 1, "d": "a", "e": 1, "f": True},
|
||||
{
|
||||
"a": 1,
|
||||
"b": 1,
|
||||
"c": 1,
|
||||
"d": "a",
|
||||
"e": 1,
|
||||
"f": True,
|
||||
"g": np.float128(1.1),
|
||||
"h": np.int64(2),
|
||||
},
|
||||
{"a": 2, "b": "a", "c": "a", "d": "a"},
|
||||
{"a": 3, "b": None, "c": 3, "d": "a", "e": 3},
|
||||
]
|
||||
expected = {"a": True, "b": False, "c": False, "d": False, "e": True, "f": False}
|
||||
expected = {
|
||||
"a": True,
|
||||
"b": False,
|
||||
"c": False,
|
||||
"d": False,
|
||||
"e": True,
|
||||
"f": False,
|
||||
"g": True,
|
||||
"h": True,
|
||||
}
|
||||
|
||||
trials = []
|
||||
for user_attrs in user_attrs_list:
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import optuna
|
||||
from optuna_dashboard._serializer import serialize_attrs
|
||||
from optuna_dashboard._serializer import serialize_study_detail
|
||||
@@ -25,6 +26,32 @@ def test_serialize_dict() -> None:
|
||||
assert len(serialized) <= 1
|
||||
|
||||
|
||||
def test_serialize_numpy_integer() -> None:
|
||||
serialized = serialize_attrs(
|
||||
{
|
||||
"int8": np.int8(1),
|
||||
"int16": np.int16(1),
|
||||
"int32": np.int32(1),
|
||||
"int64": np.int64(1),
|
||||
}
|
||||
)
|
||||
assert len(serialized) == 4
|
||||
assert all([v["value"] == "1" for v in serialized])
|
||||
|
||||
|
||||
def test_serialize_numpy_floating() -> None:
|
||||
serialized = serialize_attrs(
|
||||
{
|
||||
"float16": np.float16(1.0),
|
||||
"float32": np.float32(1.0),
|
||||
"float64": np.float64(1.0),
|
||||
"float128": np.float128(1.0),
|
||||
}
|
||||
)
|
||||
assert len(serialized) == 4
|
||||
assert all([v["value"] == "1.0" for v in serialized])
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support")
|
||||
def test_get_study_detail_is_preferential() -> None:
|
||||
storage = optuna.storages.InMemoryStorage()
|
||||
|
||||
@@ -358,7 +358,7 @@ function stableSort<T>(
|
||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number])
|
||||
stabilizedThis.sort((a, b) => {
|
||||
if (less) {
|
||||
const ascending = order == "asc"
|
||||
const ascending = order === "asc"
|
||||
const result = ascending
|
||||
? -less(a[0], b[0], ascending)
|
||||
: less(a[0], b[0], ascending)
|
||||
|
||||
@@ -78,7 +78,7 @@ const plotIntermediateValue = (
|
||||
t.state === "Pruned" &&
|
||||
t.values &&
|
||||
t.values.length > 0) ||
|
||||
t.state == "Running"
|
||||
t.state === "Running"
|
||||
)
|
||||
const plotData: Partial<plotly.PlotData>[] = filteredTrials.map((trial) => {
|
||||
const values = trial.intermediate_values.filter(
|
||||
|
||||
@@ -20,7 +20,7 @@ export const TrialTable: FC<{
|
||||
},
|
||||
]
|
||||
|
||||
if (study === null || study.directions.length == 1) {
|
||||
if (study === null || study.directions.length === 1) {
|
||||
columns.push({
|
||||
field: "values",
|
||||
label: "Value",
|
||||
|
||||
@@ -64,7 +64,7 @@ const getSchemaVersion = (db: SQLite3DB): string => {
|
||||
|
||||
const isSupportedSchema = (schemaVersion: string): boolean => {
|
||||
const lowestVersion = "v2.6.0.a" // supported: "v3.2.0.a", "v3.0.0.{a,b,c,d}", "v2.6.0.a"
|
||||
if (schemaVersion == lowestVersion) return true
|
||||
if (schemaVersion === lowestVersion) return true
|
||||
return isGreaterSchemaVersion(schemaVersion, lowestVersion)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ const isGreaterSchemaVersion = (
|
||||
|
||||
const left = Number(leftVersion)
|
||||
const right = Number(rightVersion)
|
||||
if (left == right) return leftSuffix > rightSuffix
|
||||
if (left === right) return leftSuffix > rightSuffix
|
||||
return left > right
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ const getStudies = (db: SQLite3DB, schemaVersion: string): Study[] => {
|
||||
trials.forEach((trial) => {
|
||||
const userAttrs = getTrialUserAttributes(db, trial.trial_id)
|
||||
userAttrs.forEach((attr) => {
|
||||
if (union_user_attrs.findIndex((s) => s.key === attr.key) == -1) {
|
||||
if (union_user_attrs.findIndex((s) => s.key === attr.key) === -1) {
|
||||
union_user_attrs.push({ key: attr.key, sortable: false })
|
||||
}
|
||||
})
|
||||
@@ -116,7 +116,7 @@ const getStudies = (db: SQLite3DB, schemaVersion: string): Study[] => {
|
||||
params.forEach((param) => {
|
||||
param_names.add(param.name)
|
||||
if (
|
||||
union_search_space.findIndex((s) => s.name === param.name) == -1
|
||||
union_search_space.findIndex((s) => s.name === param.name) === -1
|
||||
) {
|
||||
union_search_space.push({ name: param.name })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user