mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-23 13:30:25 +08:00
Merge branch 'main' into follow-up-551
This commit is contained in:
@@ -18,6 +18,9 @@ General APIs
|
||||
Human-in-the-loop
|
||||
-----------------
|
||||
|
||||
Form Widgets
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. autosummary::
|
||||
:toctree: _generated/
|
||||
:nosignatures:
|
||||
@@ -30,6 +33,17 @@ Human-in-the-loop
|
||||
optuna_dashboard.TextInputWidget
|
||||
optuna_dashboard.ObjectiveUserAttrRef
|
||||
|
||||
Preferential Optimization
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autosummary::
|
||||
:toctree: _generated/
|
||||
:nosignatures:
|
||||
|
||||
optuna_dashboard.preferential.create_study
|
||||
optuna_dashboard.preferential.load_study
|
||||
optuna_dashboard.preferential.PreferentialStudy
|
||||
|
||||
Streamlit
|
||||
-----------------
|
||||
|
||||
|
||||
@@ -140,7 +140,9 @@ def create_app(
|
||||
return {"reason": f"study_id={study_id} is not found"}
|
||||
|
||||
try:
|
||||
dst_study = optuna.create_study(storage=storage, study_name=dst_study_name)
|
||||
dst_study = optuna.create_study(
|
||||
storage=storage, study_name=dst_study_name, directions=src_study.directions
|
||||
)
|
||||
dst_study.add_trials(src_study.get_trials(deepcopy=False))
|
||||
except DuplicatedStudyError:
|
||||
response.status = 400 # Bad request
|
||||
|
||||
@@ -131,6 +131,7 @@ def serialize_study_detail(
|
||||
serialized: dict[str, Any] = {
|
||||
"name": summary.study_name,
|
||||
"directions": [d.name.lower() for d in summary.directions],
|
||||
"user_attrs": serialize_attrs(summary.user_attrs),
|
||||
}
|
||||
system_attrs = getattr(summary, "system_attrs", {})
|
||||
if summary.datetime_start is not None:
|
||||
|
||||
@@ -40,8 +40,8 @@ if TYPE_CHECKING:
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
ARTIFACTS_ATTR_PREFIX = "dashboard:artifacts:"
|
||||
ARTIFACTS_ATTR_PREFIX = "artifacts:"
|
||||
DASHBOARD_ARTIFACTS_ATTR_PREFIX = "dashboard:artifacts:"
|
||||
DEFAULT_MIME_TYPE = "application/octet-stream"
|
||||
BaseRequest.MEMFILE_MAX = int(
|
||||
os.environ.get("OPTUNA_DASHBOARD_MEMFILE_MAX", 1024 * 1024 * 128)
|
||||
@@ -81,6 +81,14 @@ 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]:
|
||||
trial = storage.get_trial(trial_id)
|
||||
if trial is None:
|
||||
response.status = 400
|
||||
return {"reason": "Invalid study_id or trial_id"}
|
||||
elif trial.state.is_finished():
|
||||
response.status = 400
|
||||
return {"reason": "The trial is already finished."}
|
||||
|
||||
# TODO(c-bata): Use optuna.artifacts.upload_artifact()
|
||||
if artifact_store is None:
|
||||
response.status = 400 # Bad Request
|
||||
@@ -102,14 +110,10 @@ def register_artifact_route(
|
||||
"mimetype": mimetype or DEFAULT_MIME_TYPE,
|
||||
"encoding": encoding,
|
||||
}
|
||||
attr_key = _artifact_prefix(trial_id=trial_id) + artifact_id
|
||||
storage.set_study_system_attr(study_id, attr_key, json.dumps(artifact))
|
||||
attr_key = ARTIFACTS_ATTR_PREFIX + artifact_id
|
||||
storage.set_trial_system_attr(trial_id, attr_key, json.dumps(artifact))
|
||||
response.status = 201
|
||||
|
||||
trial = storage.get_trial(trial_id)
|
||||
if trial is None:
|
||||
response.status = 400
|
||||
return {"reason": "Invalid study_id or trial_id"}
|
||||
return {
|
||||
"artifact_id": artifact_id,
|
||||
"artifacts": list_trial_artifacts(storage.get_study_system_attrs(study_id), trial),
|
||||
@@ -123,8 +127,14 @@ def register_artifact_route(
|
||||
return {"reason": "Cannot access to the artifacts."}
|
||||
artifact_store.remove(artifact_id)
|
||||
|
||||
attr_key = _artifact_prefix(trial_id) + artifact_id
|
||||
storage.set_study_system_attr(study_id, attr_key, json.dumps(None))
|
||||
# The artifact's metadata is stored in one of the following two locations:
|
||||
storage.set_study_system_attr(
|
||||
study_id, _artifact_prefix(trial_id) + artifact_id, json.dumps(None)
|
||||
)
|
||||
storage.set_trial_system_attr(
|
||||
trial_id, ARTIFACTS_ATTR_PREFIX + artifact_id, json.dumps(None)
|
||||
)
|
||||
|
||||
response.status = 204
|
||||
return {}
|
||||
|
||||
@@ -169,7 +179,6 @@ def upload_artifact(
|
||||
filename = os.path.basename(file_path)
|
||||
storage = trial.storage
|
||||
trial_id = trial._trial_id
|
||||
study_id = trial.study._study_id
|
||||
artifact_id = str(uuid.uuid4())
|
||||
guess_mimetype, guess_encoding = mimetypes.guess_type(filename)
|
||||
artifact: ArtifactMeta = {
|
||||
@@ -178,8 +187,8 @@ def upload_artifact(
|
||||
"encoding": encoding or guess_encoding,
|
||||
"filename": filename,
|
||||
}
|
||||
attr_key = _artifact_prefix(trial_id=trial_id) + artifact_id
|
||||
storage.set_study_system_attr(study_id, attr_key, json.dumps(artifact))
|
||||
attr_key = ARTIFACTS_ATTR_PREFIX + artifact_id
|
||||
storage.set_trial_system_attr(trial_id, attr_key, json.dumps(artifact))
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
backend.write(artifact_id, f)
|
||||
@@ -187,23 +196,27 @@ def upload_artifact(
|
||||
|
||||
|
||||
def _artifact_prefix(trial_id: int) -> str:
|
||||
return ARTIFACTS_ATTR_PREFIX + f"{trial_id}:"
|
||||
return DASHBOARD_ARTIFACTS_ATTR_PREFIX + f"{trial_id}:"
|
||||
|
||||
|
||||
def get_artifact_meta(
|
||||
storage: BaseStorage, study_id: int, trial_id: int, artifact_id: str
|
||||
) -> Optional[ArtifactMeta]:
|
||||
study_system_attr = storage.get_study_system_attrs(study_id)
|
||||
# Search study_system_attrs due to backward compatibility.
|
||||
study_system_attrs = storage.get_study_system_attrs(study_id)
|
||||
attr_key = _artifact_prefix(trial_id=trial_id) + artifact_id
|
||||
artifact_meta = study_system_attr.get(attr_key)
|
||||
artifact_meta = study_system_attrs.get(attr_key)
|
||||
if artifact_meta is not None:
|
||||
return json.loads(artifact_meta)
|
||||
|
||||
# Search trial_system_attrs. Note that artifacts uploaded via optuna.artifacts.upload_artifact
|
||||
# have a different trial_system_attrs key prefix.
|
||||
# See https://github.com/optuna/optuna/blob/f827582a8/optuna/artifacts/_upload.py#L71
|
||||
trial_system_attrs = storage.get_trial_system_attrs(trial_id)
|
||||
value = trial_system_attrs.get("artifacts:" + artifact_id)
|
||||
value = trial_system_attrs.get(ARTIFACTS_ATTR_PREFIX + artifact_id)
|
||||
if value is not None:
|
||||
return json.loads(value)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -221,18 +234,20 @@ def delete_all_artifacts(backend: ArtifactStore, storage: BaseStorage, study_id:
|
||||
def list_trial_artifacts(
|
||||
study_system_attrs: dict[str, Any], trial: FrozenTrial
|
||||
) -> list[ArtifactMeta]:
|
||||
# Collect ArtifactMeta from study_system_attrs due to backward compatibility.
|
||||
dashboard_artifact_metas = [
|
||||
json.loads(value)
|
||||
for key, value in study_system_attrs.items()
|
||||
if key.startswith(_artifact_prefix(trial._trial_id))
|
||||
]
|
||||
|
||||
# Collect ArtifactMeta from trial_system_attrs. Note that artifacts uploaded via
|
||||
# optuna.artifacts.upload_artifacts have a different trial_system_attrs key prefix.
|
||||
# See https://github.com/optuna/optuna/blob/f827582a8/optuna/artifacts/_upload.py#L16
|
||||
optuna_artifact_metas = [
|
||||
json.loads(value)
|
||||
for key, value in trial.system_attrs.items()
|
||||
if key.startswith("artifacts:")
|
||||
if key.startswith(ARTIFACTS_ATTR_PREFIX)
|
||||
]
|
||||
|
||||
artifact_metas = dashboard_artifact_metas + optuna_artifact_metas
|
||||
return [a for a in artifact_metas if a is not None]
|
||||
|
||||
@@ -22,27 +22,87 @@ _SYSTEM_ATTR_COMPARISON_READY = "preference:comparison_ready"
|
||||
|
||||
|
||||
class PreferentialStudy:
|
||||
"""A Study-like class for preferential optimization.
|
||||
|
||||
This object provides interfaces to create a new `Trial`_, set/get results
|
||||
of pairwise comparison called preferences.
|
||||
|
||||
.. _Trial: https://optuna.readthedocs.io/en/stable/reference/generated/\
|
||||
optuna.trial.Trial.html#optuna.trial.Trial
|
||||
|
||||
Note that the direct use of this constructor is not recommended.
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, study: optuna.Study) -> None:
|
||||
self._study = study
|
||||
|
||||
@property
|
||||
def trials(self) -> list[FrozenTrial]:
|
||||
"""Return the all trials.
|
||||
|
||||
.. seealso::
|
||||
|
||||
See `Study.trials`_ for details.
|
||||
|
||||
.. _Study.trials: https://optuna.readthedocs.io/en/stable/reference/generated/\
|
||||
optuna.study.Study.html#optuna.study.Study.trials
|
||||
|
||||
Returns:
|
||||
A list of FrozenTrial object
|
||||
"""
|
||||
return self._study.trials
|
||||
|
||||
@property
|
||||
def best_trials(self) -> list[FrozenTrial]:
|
||||
"""Return the trials that is not dominated by other trials.
|
||||
|
||||
.. seealso::
|
||||
|
||||
See `Study.best_trials`_ for details.
|
||||
|
||||
.. _Study.best_trials: https://optuna.readthedocs.io/en/stable/reference/\
|
||||
generated/optuna.study.Study.html#optuna.study.Study.best_trials
|
||||
|
||||
Returns:
|
||||
A list of FrozenTrial object
|
||||
"""
|
||||
return get_best_trials(self._study._study_id, self._study._storage)
|
||||
|
||||
@property
|
||||
def study_name(self) -> str:
|
||||
"""Return the name of the study.
|
||||
|
||||
Returns:
|
||||
A string object
|
||||
"""
|
||||
return self._study.study_name
|
||||
|
||||
@property
|
||||
def user_attrs(self) -> dict[str, Any]:
|
||||
"""Return user attributes of the study.
|
||||
|
||||
.. seealso::
|
||||
|
||||
See `Study.user_attrs`_ for details.
|
||||
|
||||
.. _Study.user_attrs: https://optuna.readthedocs.io/en/stable/reference/\
|
||||
generated/optuna.study.Study.html#optuna.study.Study.user_attrs
|
||||
|
||||
Returns:
|
||||
A dictionary containing all user attributes
|
||||
"""
|
||||
return self._study.user_attrs
|
||||
|
||||
@property
|
||||
def preferences(self) -> list[tuple[FrozenTrial, FrozenTrial]]:
|
||||
"""Return results of pairwise comparison.
|
||||
|
||||
Returns:
|
||||
A list of the pair of FrozenTrial objects. The left trial is better than the right one.
|
||||
"""
|
||||
return self.get_preferences(deepcopy=True)
|
||||
|
||||
def get_trials(
|
||||
@@ -50,15 +110,73 @@ class PreferentialStudy:
|
||||
deepcopy: bool = True,
|
||||
states: Container[optuna.trial.TrialState] | None = None,
|
||||
) -> list[FrozenTrial]:
|
||||
"""Return the trials that is not dominated by other trials.
|
||||
|
||||
.. seealso::
|
||||
|
||||
See `Study.get_trials`_ for details.
|
||||
|
||||
.. _Study.get_trials: https://optuna.readthedocs.io/en/stable/reference/\
|
||||
generated/optuna.study.Study.html#optuna.study.Study.get_trials
|
||||
|
||||
Args:
|
||||
deepcopy:
|
||||
Flag to control whether to apply ``copy.deepcopy()`` to the trials.
|
||||
Note that if you set the flag to :obj:`False`, you shouldn't mutate
|
||||
any fields of the returned trial. Otherwise the internal state of
|
||||
the study may corrupt and unexpected behavior may happen.
|
||||
states:
|
||||
Trial states to filter on. If :obj:`None`, include all states.
|
||||
|
||||
Returns:
|
||||
A list of FrozenTrial object
|
||||
"""
|
||||
return self._study.get_trials(deepcopy, states)
|
||||
|
||||
def ask(self, fixed_distributions: dict[str, BaseDistribution] | None = None) -> optuna.Trial:
|
||||
"""Create a new trial from which hyperparameters can be suggested.
|
||||
|
||||
.. seealso::
|
||||
|
||||
See `Study.ask`_ for details.
|
||||
|
||||
.. _Study.ask: https://optuna.readthedocs.io/en/stable/reference/\
|
||||
generated/optuna.study.Study.html#optuna.study.Study.ask
|
||||
|
||||
Args:
|
||||
fixed_distributions:
|
||||
A dictionary containing the parameter names and parameter's distributions. Each
|
||||
parameter in this dictionary is automatically suggested for the returned trial,
|
||||
even when the suggest method is not explicitly invoked by the user. If this
|
||||
argument is set to :obj:`None`, no parameter is automatically suggested.
|
||||
|
||||
Returns:
|
||||
A Trial object.
|
||||
"""
|
||||
return self._study.ask(fixed_distributions)
|
||||
|
||||
def add_trial(self, trial: FrozenTrial) -> None:
|
||||
"""Add a trial to the study.
|
||||
|
||||
.. seealso::
|
||||
|
||||
See `Study.add_trials()`_ for details.
|
||||
|
||||
.. _Study.add_trials(): https://optuna.readthedocs.io/en/stable/reference/\
|
||||
generated/optuna.study.Study.html#optuna.study.Study.add_trials
|
||||
"""
|
||||
self._study.add_trial(trial)
|
||||
|
||||
def add_trials(self, trials: Iterable[FrozenTrial]) -> None:
|
||||
"""Add trials to the study.
|
||||
|
||||
.. seealso::
|
||||
|
||||
See `Study.add_trials()`_ for details.
|
||||
|
||||
.. _Study.add_trials(): https://optuna.readthedocs.io/en/stable/reference/\
|
||||
generated/optuna.study.Study.html#optuna.study.Study.add_trials
|
||||
"""
|
||||
self._study.add_trials(trials)
|
||||
|
||||
def report_preference(
|
||||
@@ -66,6 +184,14 @@ class PreferentialStudy:
|
||||
better_trials: FrozenTrial | list[FrozenTrial],
|
||||
worse_trials: FrozenTrial | list[FrozenTrial],
|
||||
) -> None:
|
||||
"""Report results of pairwise comparison.
|
||||
|
||||
Args:
|
||||
better_trials:
|
||||
Trials that are better than worse_trials.
|
||||
worse_trials:
|
||||
Trials that are worse than better_trials.
|
||||
"""
|
||||
if not isinstance(better_trials, list):
|
||||
better_trials = [better_trials]
|
||||
if not isinstance(worse_trials, list):
|
||||
@@ -78,14 +204,43 @@ class PreferentialStudy:
|
||||
)
|
||||
|
||||
def get_preferences(self, *, deepcopy: bool = True) -> list[tuple[FrozenTrial, FrozenTrial]]:
|
||||
"""Return results of pairwise comparison.
|
||||
|
||||
Args:
|
||||
deepcopy:
|
||||
Flag to control whether to apply ``copy.deepcopy()`` to the trials.
|
||||
Note that if you set the flag to :obj:`False`, you shouldn't mutate
|
||||
any fields of the returned trial. Otherwise the internal state of
|
||||
the study may corrupt and unexpected behavior may happen.
|
||||
|
||||
Returns:
|
||||
A list of the pair of FrozenTrial objects. The left trial is better than the right one.
|
||||
"""
|
||||
trials = self._study.get_trials(deepcopy=deepcopy)
|
||||
preferences = get_preferences(self._study._study_id, self._study._storage)
|
||||
return [(trials[better], trials[worse]) for (better, worse) in preferences]
|
||||
|
||||
def set_user_attr(self, key: str, value: Any) -> None:
|
||||
"""Set a user attribute to the study.
|
||||
|
||||
Args:
|
||||
key: A key string of the attribute.
|
||||
value: A value of the attribute. The value should be JSON serializable.
|
||||
|
||||
.. seealso::
|
||||
|
||||
See the `tutorial for user attributes <https://optuna.readthedocs.io/en/stable/\
|
||||
tutorial/20_recipes/003_attributes.html>`_ on Optuna's documentation.
|
||||
"""
|
||||
self._study.set_user_attr(key, value)
|
||||
|
||||
def mark_comparison_ready(self, trial_or_number: optuna.Trial | int) -> None:
|
||||
"""Mark trials ready to compare.
|
||||
|
||||
Args:
|
||||
trial_or_number:
|
||||
A Trial object or trial_number.
|
||||
"""
|
||||
storage = self._study._storage
|
||||
if isinstance(trial_or_number, optuna.Trial):
|
||||
trial_id = trial_or_number._trial_id
|
||||
@@ -120,6 +275,46 @@ def create_study(
|
||||
study_name: str | None = None,
|
||||
load_if_exists: bool = False,
|
||||
) -> PreferentialStudy:
|
||||
"""Like ``optuna.create_study()``, but for preferential optimization.
|
||||
|
||||
Example:
|
||||
|
||||
.. testcode::
|
||||
|
||||
import optuna
|
||||
from optuna_dashboard.preferential import create_study
|
||||
|
||||
|
||||
study = create_study()
|
||||
trial = study.ask()
|
||||
|
||||
Args:
|
||||
storage:
|
||||
Database URL. If this argument is set to None, in-memory storage is used, and the
|
||||
:class:`~optuna_dashboard.preferential.PreferentialStudy` will not be persistent.
|
||||
|
||||
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
|
||||
|
||||
study_name:
|
||||
Study's name. If this argument is set to None, a unique name is generated
|
||||
automatically.
|
||||
|
||||
load_if_exists:
|
||||
Flag to control the behavior to handle a conflict of study names.
|
||||
In the case where a study named ``study_name`` already exists in the ``storage``,
|
||||
a :class:`~optuna.exceptions.DuplicatedStudyError` is raised if ``load_if_exists`` is
|
||||
set to :obj:`False`.
|
||||
Otherwise, the creation of the study is skipped, and the existing one is returned.
|
||||
|
||||
Returns:
|
||||
A :class:`~optuna_dashboard.preferential.PreferentialStudy` object.
|
||||
"""
|
||||
try:
|
||||
study = optuna.create_study(
|
||||
storage=storage,
|
||||
@@ -155,6 +350,52 @@ def load_study(
|
||||
storage: str | optuna.storages.BaseStorage,
|
||||
sampler: BaseSampler | None = None,
|
||||
) -> PreferentialStudy:
|
||||
"""Like ``optuna.load_study()``, but for preferential optimization.
|
||||
|
||||
Example:
|
||||
|
||||
.. testsetup::
|
||||
|
||||
import os
|
||||
|
||||
if os.path.exists("example.db"):
|
||||
raise RuntimeError("'example.db' already exists. Please remove it.")
|
||||
|
||||
.. testcode::
|
||||
|
||||
import optuna
|
||||
from optuna_dashboard.preferential import create_study
|
||||
from optuna_dashboard.preferential import load_study
|
||||
|
||||
study = create_study(storage="sqlite:///example.db", study_name="my_study")
|
||||
study.ask()
|
||||
|
||||
loaded_study = load_study(study_name="my_study", storage="sqlite:///example.db")
|
||||
assert len(loaded_study.trials) == len(study.trials)
|
||||
|
||||
.. testcleanup::
|
||||
|
||||
os.remove("example.db")
|
||||
|
||||
Args:
|
||||
study_name:
|
||||
Study's name. Each study has a unique name as an identifier. If :obj:`None`, checks
|
||||
whether the storage contains a single study, and if so loads that study.
|
||||
``study_name`` is required if there are multiple studies in the storage.
|
||||
storage:
|
||||
Database URL such as ``sqlite:///example.db``. Please see also the documentation of
|
||||
: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
|
||||
|
||||
Returns:
|
||||
A :class:`~optuna_dashboard.preferential.PreferentialStudy` object.
|
||||
"""
|
||||
study = optuna.load_study(
|
||||
study_name=study_name, storage=storage, sampler=sampler or RandomSampler()
|
||||
)
|
||||
|
||||
@@ -59,6 +59,7 @@ interface StudyDetailResponse {
|
||||
name: string
|
||||
datetime_start: string
|
||||
directions: StudyDirection[]
|
||||
user_attrs: Attribute[]
|
||||
trials: TrialResponse[]
|
||||
best_trials: TrialResponse[]
|
||||
intersection_search_space: SearchSpaceItem[]
|
||||
@@ -93,6 +94,7 @@ export const getStudyDetailAPI = (
|
||||
name: res.data.name,
|
||||
datetime_start: new Date(res.data.datetime_start),
|
||||
directions: res.data.directions,
|
||||
user_attrs: res.data.user_attrs,
|
||||
trials: trials,
|
||||
best_trials: best_trials,
|
||||
union_search_space: res.data.union_search_space,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
useTheme,
|
||||
IconButton,
|
||||
} from "@mui/material"
|
||||
import Grid2 from "@mui/material/Unstable_Grid2"
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight"
|
||||
import Chip from "@mui/material/Chip"
|
||||
import FormControlLabel from "@mui/material/FormControlLabel"
|
||||
@@ -26,7 +27,7 @@ import HomeIcon from "@mui/icons-material/Home"
|
||||
import { actionCreator } from "../action"
|
||||
import { studySummariesState, studyDetailsState } from "../state"
|
||||
import { AppDrawer } from "./AppDrawer"
|
||||
import { GraphEdfMultiStudies } from "./GraphEdf"
|
||||
import { GraphEdf } from "./GraphEdf"
|
||||
import { GraphHistory } from "./GraphHistory"
|
||||
import { useNavigate, useLocation } from "react-router-dom"
|
||||
|
||||
@@ -325,19 +326,21 @@ const StudiesGraph: FC<{ studies: StudySummary[] }> = ({ studies }) => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
{showStudyDetails !== null &&
|
||||
showStudyDetails.length > 0 &&
|
||||
showStudyDetails.every((s) => s) ? (
|
||||
<Card
|
||||
sx={{
|
||||
margin: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<GraphEdfMultiStudies studies={showStudyDetails} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
<Grid2 container spacing={2} sx={{ padding: theme.spacing(0, 2) }}>
|
||||
{showStudyDetails !== null &&
|
||||
showStudyDetails.length > 0 &&
|
||||
showStudyDetails.every((s) => s)
|
||||
? showStudyDetails[0].directions.map((d, i) => (
|
||||
<Grid2 xs={6} key={i}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<GraphEdf studies={showStudyDetails} objectiveId={i} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
))
|
||||
: null}
|
||||
</Grid2>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import React, { FC, useEffect, useMemo } from "react"
|
||||
import {
|
||||
Grid,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Typography,
|
||||
SelectChangeEvent,
|
||||
useTheme,
|
||||
Box,
|
||||
} from "@mui/material"
|
||||
import { Typography, useTheme, Box } from "@mui/material"
|
||||
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
|
||||
import {
|
||||
Target,
|
||||
useFilteredTrials,
|
||||
useFilteredTrialsFromStudies,
|
||||
useObjectiveTargets,
|
||||
} from "../trialFilter"
|
||||
import { Target, useFilteredTrialsFromStudies } from "../trialFilter"
|
||||
|
||||
const plotDomId = "graph-edf"
|
||||
const getPlotDomId = (objectiveId: number) => `graph-edf-${objectiveId}`
|
||||
|
||||
interface EdfPlotInfo {
|
||||
@@ -28,44 +12,16 @@ interface EdfPlotInfo {
|
||||
}
|
||||
|
||||
export const GraphEdf: FC<{
|
||||
study: StudyDetail | null
|
||||
studies: StudyDetail[]
|
||||
objectiveId: number
|
||||
}> = ({ study, objectiveId }) => {
|
||||
}> = ({ studies, objectiveId }) => {
|
||||
const theme = useTheme()
|
||||
const domId = getPlotDomId(objectiveId)
|
||||
const target = useMemo<Target>(
|
||||
() => new Target("objective", objectiveId),
|
||||
[objectiveId]
|
||||
)
|
||||
const trials = useFilteredTrials(study, [target], false)
|
||||
|
||||
useEffect(() => {
|
||||
if (study !== null) {
|
||||
plotEdf(trials, target, domId, theme.palette.mode)
|
||||
}
|
||||
}, [trials, target, domId, theme.palette.mode])
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
|
||||
>
|
||||
{`EDF for ${target.toLabel(study?.objective_names)}`}
|
||||
</Typography>
|
||||
<Box id={domId} sx={{ height: "450px" }} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export const GraphEdfMultiStudies: FC<{
|
||||
studies: StudyDetail[]
|
||||
}> = ({ studies }) => {
|
||||
const theme = useTheme()
|
||||
const [targets, selected, setTarget] = useObjectiveTargets(
|
||||
studies.length !== 0 ? studies[0] : null
|
||||
)
|
||||
|
||||
const trials = useFilteredTrialsFromStudies(studies, [selected], false)
|
||||
const trials = useFilteredTrialsFromStudies(studies, [target], false)
|
||||
const edfPlotInfos = studies.map((study, index) => {
|
||||
const e: EdfPlotInfo = {
|
||||
study_name: study?.name,
|
||||
@@ -74,112 +30,24 @@ export const GraphEdfMultiStudies: FC<{
|
||||
return e
|
||||
})
|
||||
|
||||
const handleObjectiveChange = (event: SelectChangeEvent<string>) => {
|
||||
setTarget(event.target.value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
plotEdfMultiStudies(edfPlotInfos, selected, plotDomId, theme.palette.mode)
|
||||
}, [studies, selected, theme.palette.mode])
|
||||
plotEdf(edfPlotInfos, target, domId, theme.palette.mode)
|
||||
}, [studies, target, theme.palette.mode])
|
||||
|
||||
return (
|
||||
<Grid container direction="row">
|
||||
<Grid
|
||||
item
|
||||
xs={3}
|
||||
container
|
||||
direction="column"
|
||||
sx={{ paddingRight: theme.spacing(2) }}
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
|
||||
>
|
||||
EDF
|
||||
</Typography>
|
||||
{studies.length > 0 && studies[0].directions.length !== 1 ? (
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Objective:</FormLabel>
|
||||
<Select
|
||||
value={selected.identifier()}
|
||||
onChange={handleObjectiveChange}
|
||||
>
|
||||
{targets.map((target, i) => (
|
||||
<MenuItem value={target.identifier()} key={i}>
|
||||
{target.toLabel(studies[0].objective_names)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
) : null}
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<Box id={plotDomId} sx={{ height: "450px" }} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
{`EDF for ${target.toLabel(studies[0].objective_names)}`}
|
||||
</Typography>
|
||||
<Box id={domId} sx={{ height: "450px" }} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const plotEdf = (
|
||||
trials: Trial[],
|
||||
target: Target,
|
||||
domId: string,
|
||||
mode: string
|
||||
) => {
|
||||
if (document.getElementById(domId) === null) {
|
||||
return
|
||||
}
|
||||
if (trials.length === 0) {
|
||||
plotly.react(domId, [], {
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const target_name = "Objective Value"
|
||||
const layout: Partial<plotly.Layout> = {
|
||||
xaxis: {
|
||||
title: target_name,
|
||||
},
|
||||
yaxis: {
|
||||
title: "Cumulative Probability",
|
||||
},
|
||||
margin: {
|
||||
l: 50,
|
||||
t: 0,
|
||||
r: 50,
|
||||
b: 50,
|
||||
},
|
||||
uirevision: "true",
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
}
|
||||
|
||||
const values = trials.map((t) => target.getTargetValue(t) as number)
|
||||
const numValues = values.length
|
||||
const minX = Math.min(...values)
|
||||
const maxX = Math.max(...values)
|
||||
const numStep = 100
|
||||
const _step = (maxX - minX) / (numStep - 1)
|
||||
|
||||
const xValues = []
|
||||
const yValues = []
|
||||
for (let i = 0; i < numStep; i++) {
|
||||
const boundary_right = minX + _step * i
|
||||
xValues.push(boundary_right)
|
||||
yValues.push(values.filter((v) => v <= boundary_right).length / numValues)
|
||||
}
|
||||
|
||||
const plotData: Partial<plotly.PlotData>[] = [
|
||||
{
|
||||
type: "scatter",
|
||||
x: xValues,
|
||||
y: yValues,
|
||||
},
|
||||
]
|
||||
plotly.react(domId, plotData, layout)
|
||||
}
|
||||
|
||||
const plotEdfMultiStudies = (
|
||||
edfPlotInfos: EdfPlotInfo[],
|
||||
target: Target,
|
||||
domId: string,
|
||||
|
||||
@@ -127,7 +127,7 @@ export const StudyDetail: FC<{
|
||||
<Grid2 xs={6} key={i}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<GraphEdf study={studyDetail} objectiveId={i} />
|
||||
<GraphEdf studies={[studyDetail]} objectiveId={i} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
|
||||
@@ -39,7 +39,7 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
setIncludePruned(!includePruned)
|
||||
}
|
||||
|
||||
const userAttrs = studySummary?.user_attrs || []
|
||||
const userAttrs = studySummary?.user_attrs || studyDetail?.user_attrs || []
|
||||
const userAttrColumns: DataGridColumn<Attribute>[] = [
|
||||
{ field: "key", label: "Key", sortable: true },
|
||||
{ field: "value", label: "Value", sortable: true },
|
||||
|
||||
@@ -710,55 +710,57 @@ const TrialArtifact: FC<{ trial: Trial }> = ({ trial }) => {
|
||||
)
|
||||
}
|
||||
})}
|
||||
<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}
|
||||
{trial.state === "Running" || trial.state === "Waiting" ? (
|
||||
<Card
|
||||
sx={{
|
||||
height: "100%",
|
||||
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}
|
||||
>
|
||||
<CardContent
|
||||
<CardActionArea
|
||||
onClick={handleClick}
|
||||
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 }}
|
||||
<CardContent
|
||||
sx={{
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
Drag your file here or click to browse.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
<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>
|
||||
) : null}
|
||||
</Box>
|
||||
{renderDeleteArtifactDialog()}
|
||||
</>
|
||||
|
||||
Vendored
+1
@@ -185,6 +185,7 @@ type StudyDetail = {
|
||||
id: number
|
||||
name: string
|
||||
directions: StudyDirection[]
|
||||
user_attrs: Attribute[]
|
||||
datetime_start: Date
|
||||
best_trials: Trial[]
|
||||
trials: Trial[]
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from optuna.storages import BaseStorage
|
||||
from optuna_dashboard.artifact import _backend
|
||||
import pytest
|
||||
|
||||
|
||||
def test_get_artifact_path() -> None:
|
||||
study = MagicMock(_study_id=0)
|
||||
trial = MagicMock(_trial_id=0, study=study)
|
||||
assert _backend.get_artifact_path(trial=trial, artifact_id="id0") == "/artifacts/0/0/id0"
|
||||
|
||||
|
||||
def test_artifact_prefix() -> None:
|
||||
actual = _backend._artifact_prefix(trial_id=0)
|
||||
assert actual == "dashboard:artifacts:0:"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def init_storage_with_artifact_meta() -> BaseStorage:
|
||||
from optuna import create_study
|
||||
from optuna.storages import InMemoryStorage
|
||||
|
||||
storage = InMemoryStorage()
|
||||
study = create_study(storage=storage)
|
||||
|
||||
study_system_attrs = {
|
||||
"dashboard:artifacts:0:id0": '{"artifact_id": "id0", "filename": "foo.txt"}',
|
||||
"dashboard:artifacts:0:id1": '{"artifact_id": "id1", "filename": "bar.txt"}',
|
||||
"baz": "baz",
|
||||
}
|
||||
for key, value in study_system_attrs.items():
|
||||
study.set_system_attr(key, value)
|
||||
|
||||
trial_system_attrs = {
|
||||
"artifacts:id2": '{"artifact_id": "id2", "filename": "baz.txt"}',
|
||||
"artifacts:id3": '{"artifact_id": "id3", "filename": "qux.txt"}',
|
||||
}
|
||||
for key, value in trial_system_attrs.items():
|
||||
trial = study.ask()
|
||||
trial.set_system_attr(key, value)
|
||||
study.tell(trial, 0.0)
|
||||
|
||||
return storage
|
||||
|
||||
|
||||
def test_get_artifact_meta(init_storage_with_artifact_meta: MagicMock) -> None:
|
||||
storage = init_storage_with_artifact_meta
|
||||
|
||||
actual = _backend.get_artifact_meta(storage, study_id=0, trial_id=0, artifact_id="id0")
|
||||
assert actual == {"artifact_id": "id0", "filename": "foo.txt"}
|
||||
|
||||
actual = _backend.get_artifact_meta(storage, study_id=0, trial_id=1, artifact_id="id3")
|
||||
assert actual == {"artifact_id": "id3", "filename": "qux.txt"}
|
||||
|
||||
actual = _backend.get_artifact_meta(storage, study_id=0, trial_id=0, artifact_id="id4")
|
||||
assert actual is None
|
||||
|
||||
|
||||
def test_delete_all_artifacts(init_storage_with_artifact_meta: MagicMock) -> None:
|
||||
backend = MagicMock()
|
||||
storage = init_storage_with_artifact_meta
|
||||
_backend.delete_all_artifacts(backend, storage, study_id=0)
|
||||
|
||||
assert backend.remove.call_args_list == [
|
||||
(("id0",),),
|
||||
(("id1",),),
|
||||
(("id2",),),
|
||||
(("id3",),),
|
||||
]
|
||||
|
||||
|
||||
def test_list_trial_artifacts(init_storage_with_artifact_meta: MagicMock) -> None:
|
||||
storage = init_storage_with_artifact_meta
|
||||
trial = MagicMock(_trial_id=0, system_attrs=storage.get_trial_system_attrs(0))
|
||||
|
||||
actual = _backend.list_trial_artifacts(storage.get_study_system_attrs(0), trial)
|
||||
assert actual == [
|
||||
{"artifact_id": "id0", "filename": "foo.txt"},
|
||||
{"artifact_id": "id1", "filename": "bar.txt"},
|
||||
{"artifact_id": "id2", "filename": "baz.txt"},
|
||||
]
|
||||
@@ -0,0 +1,100 @@
|
||||
import * as plotly from "plotly.js-dist-min"
|
||||
import React, { FC, useEffect } from "react"
|
||||
import { Box, Typography, useTheme, CardContent, Card } from "@mui/material"
|
||||
import { plotlyDarkTemplate } from "../PlotlyDarkMode"
|
||||
|
||||
const plotDomId = "graph-intermediate-values"
|
||||
|
||||
export const PlotIntermediateValues: FC<{
|
||||
trials: Trial[]
|
||||
includePruned: boolean
|
||||
logScale: boolean
|
||||
}> = ({ trials, includePruned, logScale }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
plotIntermediateValue(
|
||||
trials,
|
||||
theme.palette.mode,
|
||||
false,
|
||||
!includePruned,
|
||||
logScale
|
||||
)
|
||||
}, [trials, theme.palette.mode, false, includePruned, logScale])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: "1em 0", fontWeight: theme.typography.fontWeightBold }}
|
||||
>
|
||||
Intermediate values
|
||||
</Typography>
|
||||
<Box id={plotDomId} sx={{ height: "450px" }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const plotIntermediateValue = (
|
||||
trials: Trial[],
|
||||
mode: string,
|
||||
filterCompleteTrial: boolean,
|
||||
filterPrunedTrial: boolean,
|
||||
logScale: boolean
|
||||
) => {
|
||||
if (document.getElementById(plotDomId) === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const layout: Partial<plotly.Layout> = {
|
||||
margin: {
|
||||
l: 50,
|
||||
t: 0,
|
||||
r: 50,
|
||||
b: 0,
|
||||
},
|
||||
yaxis: {
|
||||
title: "Objective Value",
|
||||
type: logScale ? "log" : "linear",
|
||||
},
|
||||
xaxis: {
|
||||
title: "Step",
|
||||
type: "linear",
|
||||
},
|
||||
uirevision: "true",
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
}
|
||||
if (trials.length === 0) {
|
||||
plotly.react(plotDomId, [], layout)
|
||||
return
|
||||
}
|
||||
|
||||
const filteredTrials = trials.filter(
|
||||
(t) =>
|
||||
(!filterCompleteTrial && t.state === "Complete") ||
|
||||
(!filterPrunedTrial &&
|
||||
t.state === "Pruned" &&
|
||||
t.values &&
|
||||
t.values.length > 0) ||
|
||||
t.state == "Running"
|
||||
)
|
||||
const plotData: Partial<plotly.PlotData>[] = filteredTrials.map((trial) => {
|
||||
const values = trial.intermediate_values.filter(
|
||||
(iv) => iv.value !== "inf" && iv.value !== "-inf" && iv.value !== "nan"
|
||||
)
|
||||
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)`,
|
||||
}
|
||||
})
|
||||
plotly.react(plotDomId, plotData, layout)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
} from "@mui/material"
|
||||
import Grid2 from "@mui/material/Unstable_Grid2"
|
||||
import { Home } from "@mui/icons-material"
|
||||
import Brightness4Icon from "@mui/icons-material/Brightness4"
|
||||
import Brightness7Icon from "@mui/icons-material/Brightness7"
|
||||
@@ -19,6 +20,7 @@ import { studiesState } from "../state"
|
||||
import { TrialTable } from "./TrialTable"
|
||||
import { PlotHistory } from "./PlotHistory"
|
||||
import { PlotImportance } from "./PlotImportance"
|
||||
import { PlotIntermediateValues } from "./PlotIntermediateValues"
|
||||
|
||||
const useStudyValue = (idx: number): Study | null => {
|
||||
const studies = useRecoilValue<Study[]>(studiesState)
|
||||
@@ -83,7 +85,7 @@ export const StudyDetail: FC<{
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
@@ -102,17 +104,34 @@ export const StudyDetail: FC<{
|
||||
<PlotHistory study={study} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
{!!study && <PlotImportance study={study} />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Grid2 container spacing={0}>
|
||||
<Grid2 xs={6}>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
{!!study && <PlotImportance study={study} />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
<Grid2 xs={6}>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
{!!study && (
|
||||
<PlotIntermediateValues
|
||||
trials={study.trials}
|
||||
includePruned={false}
|
||||
logScale={false}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
{!!study && <TrialTable study={study} initialRowsPerPage={10} />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -20,36 +20,6 @@ export const TrialTable: FC<{
|
||||
},
|
||||
]
|
||||
|
||||
study.union_search_space.forEach((s) => {
|
||||
columns.push({
|
||||
field: "params",
|
||||
label: `Param ${s.name}`,
|
||||
toCellValue: (i) =>
|
||||
trials[i].params.find((p) => p.name === s.name)?.param_internal_value ||
|
||||
null,
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
less: (firstEl, secondEl): number => {
|
||||
const firstVal = firstEl.params.find(
|
||||
(p) => p.name === s.name
|
||||
)?.param_internal_value
|
||||
const secondVal = secondEl.params.find(
|
||||
(p) => p.name === s.name
|
||||
)?.param_internal_value
|
||||
|
||||
if (firstVal === secondVal) {
|
||||
return 0
|
||||
} else if (firstVal && secondVal) {
|
||||
return firstVal < secondVal ? 1 : -1
|
||||
} else if (firstVal) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
if (study === null || study.directions.length == 1) {
|
||||
columns.push({
|
||||
field: "values",
|
||||
@@ -117,6 +87,66 @@ export const TrialTable: FC<{
|
||||
columns.push(...objectiveColumns)
|
||||
}
|
||||
|
||||
study.union_search_space.forEach((s) => {
|
||||
columns.push({
|
||||
field: "params",
|
||||
label: `Param ${s.name}`,
|
||||
toCellValue: (i) =>
|
||||
trials[i].params.find((p) => p.name === s.name)?.param_external_value ??
|
||||
null,
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
less: (firstEl, secondEl): number => {
|
||||
const firstVal = firstEl.params.find(
|
||||
(p) => p.name === s.name
|
||||
)?.param_internal_value
|
||||
const secondVal = secondEl.params.find(
|
||||
(p) => p.name === s.name
|
||||
)?.param_internal_value
|
||||
|
||||
if (firstVal === secondVal) {
|
||||
return 0
|
||||
} else if (firstVal && secondVal) {
|
||||
return firstVal < secondVal ? 1 : -1
|
||||
} else if (firstVal) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
study.union_user_attrs.forEach((attr_spec) => {
|
||||
columns.push({
|
||||
field: "user_attrs",
|
||||
label: `UserAttribute ${attr_spec.key}`,
|
||||
toCellValue: (i) =>
|
||||
trials[i].user_attrs.find((attr) => attr.key === attr_spec.key)
|
||||
?.value || null,
|
||||
sortable: attr_spec.sortable,
|
||||
filterable: false,
|
||||
less: (firstEl, secondEl): number => {
|
||||
const firstVal = firstEl.user_attrs.find(
|
||||
(attr) => attr.key === attr_spec.key
|
||||
)?.value
|
||||
const secondVal = secondEl.user_attrs.find(
|
||||
(attr) => attr.key === attr_spec.key
|
||||
)?.value
|
||||
|
||||
if (firstVal === secondVal) {
|
||||
return 0
|
||||
} else if (firstVal && secondVal) {
|
||||
return firstVal < secondVal ? 1 : -1
|
||||
} else if (firstVal) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<DataGrid<Trial>
|
||||
columns={columns}
|
||||
|
||||
+272
-144
@@ -2,6 +2,14 @@
|
||||
import sqlite3InitModule from "@sqlite.org/sqlite-wasm"
|
||||
import { SetterOrUpdater } from "recoil"
|
||||
|
||||
type SQLite3DB = {
|
||||
exec(options: {
|
||||
sql: string
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callback: (...args: any[]) => void
|
||||
}): void
|
||||
}
|
||||
|
||||
export const loadStorage = (
|
||||
arrayBuffer: ArrayBuffer,
|
||||
setter: SetterOrUpdater<Study[]>
|
||||
@@ -30,155 +38,275 @@ export const loadStorage = (
|
||||
)
|
||||
db.checkRc(rc)
|
||||
try {
|
||||
// Check version_info table
|
||||
let supported = true
|
||||
db.exec({
|
||||
sql: "SELECT schema_version FROM version_info LIMIT 1",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callback: (vals: any[]) => {
|
||||
if (vals[0] != 12) {
|
||||
supported = false
|
||||
}
|
||||
},
|
||||
})
|
||||
if (!supported) {
|
||||
if (!isSupportedSchema(db)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get studies
|
||||
const studies: Study[] = []
|
||||
db.exec({
|
||||
sql:
|
||||
"SELECT s.study_id, s.study_name, sd.direction, sd.objective" +
|
||||
" FROM studies AS s INNER JOIN study_directions AS sd" +
|
||||
" ON s.study_id = sd.study_id ORDER BY sd.study_direction_id",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callback: (vals: any[]) => {
|
||||
const study_id = vals[0]
|
||||
const study_name = vals[1]
|
||||
const direction: StudyDirection = vals[2].toLowerCase()
|
||||
const objective = vals[3]
|
||||
let index = 0
|
||||
|
||||
if (objective === 0) {
|
||||
studies.push({
|
||||
study_id: study_id,
|
||||
study_name: study_name,
|
||||
directions: [direction],
|
||||
union_search_space: [],
|
||||
intersection_search_space: [],
|
||||
user_attrs: [],
|
||||
system_attrs: [],
|
||||
trials: [],
|
||||
})
|
||||
} else {
|
||||
index = studies.findIndex((s) => s.study_id === study_id)
|
||||
studies[index].directions.push(direction)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
studies.forEach((s) => {
|
||||
db.exec({
|
||||
sql:
|
||||
"SELECT t.trial_id, t.number, t.study_id, t.state, t.datetime_start, t.datetime_complete," +
|
||||
" tv.objective, tv.value, tv.value_type" +
|
||||
" FROM trials AS t LEFT JOIN trial_values AS tv ON tv.trial_id = t.trial_id" +
|
||||
` WHERE t.study_id = ${s.study_id}` +
|
||||
" ORDER BY t.number",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callback: (vals: any[]) => {
|
||||
const state: TrialState =
|
||||
vals[3] === "COMPLETE"
|
||||
? "Complete"
|
||||
: vals[3] === "PRUNED"
|
||||
? "Pruned"
|
||||
: vals[3] === "RUNNING"
|
||||
? "Running"
|
||||
: vals[3] === "WAITING"
|
||||
? "Waiting"
|
||||
: "Fail"
|
||||
const trial: Trial = {
|
||||
trial_id: vals[0],
|
||||
number: vals[1],
|
||||
study_id: vals[2],
|
||||
state: state,
|
||||
params: [],
|
||||
intermediate_values: [],
|
||||
user_attrs: [],
|
||||
system_attrs: [],
|
||||
}
|
||||
s.trials.push(trial)
|
||||
},
|
||||
})
|
||||
const union_search_space: SearchSpaceItem[] = []
|
||||
let intersection_search_space: Set<SearchSpaceItem> = new Set()
|
||||
s.trials.forEach((trial) => {
|
||||
const params: TrialParam[] = []
|
||||
const param_names = new Set<string>()
|
||||
db.exec({
|
||||
sql:
|
||||
"SELECT param_name, param_value" +
|
||||
` FROM trial_params WHERE trial_id = ${trial.trial_id}`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callback: (vals: any[]) => {
|
||||
const param_name = vals[0]
|
||||
params.push({
|
||||
name: param_name,
|
||||
param_internal_value: vals[1],
|
||||
})
|
||||
|
||||
param_names.add(param_name)
|
||||
if (
|
||||
union_search_space.findIndex((s) => s.name === param_name) == -1
|
||||
) {
|
||||
union_search_space.push({ name: param_name })
|
||||
}
|
||||
},
|
||||
})
|
||||
if (intersection_search_space.size === 0) {
|
||||
param_names.forEach((s) => {
|
||||
intersection_search_space.add({
|
||||
name: s,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
intersection_search_space = new Set(
|
||||
Array.from(intersection_search_space).filter((s) =>
|
||||
param_names.has(s.name)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
trial.params = params
|
||||
const values: TrialValueNumber[] = []
|
||||
db.exec({
|
||||
sql:
|
||||
"SELECT value, value_type" +
|
||||
` FROM trial_values WHERE trial_id = ${trial.trial_id}` +
|
||||
" 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 (s.directions.length === values.length) {
|
||||
trial.values = values
|
||||
}
|
||||
})
|
||||
s.union_search_space = union_search_space
|
||||
s.intersection_search_space = Array.from(intersection_search_space)
|
||||
})
|
||||
|
||||
const studies = getStudies(db)
|
||||
setter((prev) => [...prev, ...studies])
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isSupportedSchema = (db: SQLite3DB): boolean => {
|
||||
let supported = true
|
||||
db.exec({
|
||||
sql: "SELECT schema_version FROM version_info LIMIT 1",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callback: (vals: any[]) => {
|
||||
if (vals[0] != 12) {
|
||||
supported = false
|
||||
}
|
||||
},
|
||||
})
|
||||
return supported
|
||||
}
|
||||
|
||||
const getStudies = (db: SQLite3DB): Study[] => {
|
||||
const studies: Study[] = []
|
||||
db.exec({
|
||||
sql:
|
||||
"SELECT s.study_id, s.study_name, sd.direction, sd.objective" +
|
||||
" FROM studies AS s INNER JOIN study_directions AS sd" +
|
||||
" ON s.study_id = sd.study_id ORDER BY sd.study_direction_id",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callback: (vals: any[]) => {
|
||||
const studyId = vals[0]
|
||||
const studyName = vals[1]
|
||||
const direction: StudyDirection =
|
||||
vals[2] === "MINIMIZE" ? "minimize" : "maximize"
|
||||
const objective = vals[3]
|
||||
|
||||
const trials = getTrials(db, studyId)
|
||||
const union_search_space: SearchSpaceItem[] = []
|
||||
const union_user_attrs: AttributeSpec[] = []
|
||||
let intersection_search_space: Set<SearchSpaceItem> = new Set()
|
||||
trials.forEach((trial) => {
|
||||
const userAttrs = getTrialUserAttributes(db, trial.trial_id)
|
||||
userAttrs.forEach((attr) => {
|
||||
if (union_user_attrs.findIndex((s) => s.key === attr.key) == -1) {
|
||||
union_user_attrs.push({ key: attr.key, sortable: false })
|
||||
}
|
||||
})
|
||||
|
||||
const params = getTrialParams(db, trial.trial_id)
|
||||
const param_names = new Set<string>()
|
||||
params.forEach((param) => {
|
||||
param_names.add(param.name)
|
||||
if (
|
||||
union_search_space.findIndex((s) => s.name === param.name) == -1
|
||||
) {
|
||||
union_search_space.push({ name: param.name })
|
||||
}
|
||||
})
|
||||
if (intersection_search_space.size === 0) {
|
||||
param_names.forEach((s) => {
|
||||
intersection_search_space.add({
|
||||
name: s,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
intersection_search_space = new Set(
|
||||
Array.from(intersection_search_space).filter((s) =>
|
||||
param_names.has(s.name)
|
||||
)
|
||||
)
|
||||
}
|
||||
trial.params = params
|
||||
trial.user_attrs = userAttrs
|
||||
})
|
||||
|
||||
if (objective === 0) {
|
||||
studies.push({
|
||||
study_id: studyId,
|
||||
study_name: studyName,
|
||||
directions: [direction],
|
||||
union_search_space: union_search_space,
|
||||
intersection_search_space: Array.from(intersection_search_space),
|
||||
union_user_attrs: union_user_attrs,
|
||||
trials: trials,
|
||||
})
|
||||
return
|
||||
}
|
||||
const index = studies.findIndex((s) => s.study_id === studyId)
|
||||
studies[index].directions.push(direction)
|
||||
},
|
||||
})
|
||||
return studies
|
||||
}
|
||||
|
||||
const getTrials = (db: SQLite3DB, studyId: number): Trial[] => {
|
||||
const trials: Trial[] = []
|
||||
db.exec({
|
||||
sql:
|
||||
"SELECT trial_id, number, state, datetime_start, datetime_complete FROM trials" +
|
||||
` WHERE study_id = ${studyId} ORDER BY number`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callback: (vals: any[]) => {
|
||||
const trialId = vals[0]
|
||||
const state: TrialState =
|
||||
vals[2] === "COMPLETE"
|
||||
? "Complete"
|
||||
: vals[2] === "PRUNED"
|
||||
? "Pruned"
|
||||
: vals[2] === "RUNNING"
|
||||
? "Running"
|
||||
: vals[2] === "WAITING"
|
||||
? "Waiting"
|
||||
: "Fail"
|
||||
const trial: Trial = {
|
||||
trial_id: trialId,
|
||||
number: vals[1],
|
||||
study_id: studyId,
|
||||
state: state,
|
||||
values: getTrialValues(db, trialId),
|
||||
intermediate_values: getTrialIntermediateValues(db, trialId),
|
||||
params: [], // Set this column later
|
||||
user_attrs: [], // Set this column later
|
||||
datetime_start: vals[3],
|
||||
datetime_complete: vals[4],
|
||||
}
|
||||
trials.push(trial)
|
||||
},
|
||||
})
|
||||
return trials
|
||||
}
|
||||
|
||||
const getTrialValues = (db: SQLite3DB, trialId: number): 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]
|
||||
)
|
||||
},
|
||||
})
|
||||
return values
|
||||
}
|
||||
|
||||
const getTrialParams = (db: SQLite3DB, trialId: number): TrialParam[] => {
|
||||
const params: TrialParam[] = []
|
||||
db.exec({
|
||||
sql:
|
||||
"SELECT param_name, param_value, distribution_json" +
|
||||
` FROM trial_params WHERE trial_id = ${trialId}`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callback: (vals: any[]) => {
|
||||
const distribution = parseDistributionJSON(vals[2])
|
||||
params.push({
|
||||
name: vals[0],
|
||||
param_internal_value: vals[1],
|
||||
param_external_type: distribution.type,
|
||||
param_external_value: paramInternalValueToExternalValue(
|
||||
distribution,
|
||||
vals[1]
|
||||
),
|
||||
distribution: distribution,
|
||||
})
|
||||
},
|
||||
})
|
||||
return params
|
||||
}
|
||||
|
||||
const paramInternalValueToExternalValue = (
|
||||
distribution: Distribution,
|
||||
internalValue: number
|
||||
): string => {
|
||||
if (distribution.type === "FloatDistribution") {
|
||||
return internalValue.toString()
|
||||
} else if (distribution.type === "IntDistribution") {
|
||||
return internalValue.toString()
|
||||
} else {
|
||||
return distribution.choices[internalValue].value
|
||||
}
|
||||
}
|
||||
|
||||
const parseDistributionJSON = (t: string): Distribution => {
|
||||
const parsed = JSON.parse(t)
|
||||
if (parsed.name === "FloatDistribution") {
|
||||
return {
|
||||
type: "FloatDistribution",
|
||||
low: parsed.attributes.low as number,
|
||||
high: parsed.attributes.high as number,
|
||||
step: parsed.attributes.step as number,
|
||||
log: parsed.attributes.log as boolean,
|
||||
}
|
||||
} else if (parsed.name === "IntDistribution") {
|
||||
return {
|
||||
type: "IntDistribution",
|
||||
low: parsed.attributes.low as number,
|
||||
high: parsed.attributes.high as number,
|
||||
step: parsed.attributes.step as number,
|
||||
log: parsed.attributes.log as boolean,
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const choices = parsed.attributes.choices.map((value: any) => {
|
||||
// TODO(c-bata): Support other types
|
||||
return {
|
||||
pytype: "str",
|
||||
value: value.toString(),
|
||||
}
|
||||
})
|
||||
return {
|
||||
type: "CategoricalDistribution",
|
||||
choices: choices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getTrialUserAttributes = (
|
||||
db: SQLite3DB,
|
||||
trialId: number
|
||||
): Attribute[] => {
|
||||
const attrs: Attribute[] = []
|
||||
db.exec({
|
||||
sql:
|
||||
"SELECT key, value_json" +
|
||||
` FROM trial_user_attributes WHERE trial_id = ${trialId}`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callback: (vals: any[]) => {
|
||||
attrs.push({
|
||||
key: vals[0],
|
||||
value: vals[1],
|
||||
})
|
||||
},
|
||||
})
|
||||
return attrs
|
||||
}
|
||||
|
||||
const getTrialIntermediateValues = (
|
||||
db: SQLite3DB,
|
||||
trialId: number
|
||||
): 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],
|
||||
})
|
||||
},
|
||||
})
|
||||
return values
|
||||
}
|
||||
|
||||
Vendored
+15
-4
@@ -27,6 +27,11 @@ type CategoricalDistribution = {
|
||||
choices: { pytype: string; value: string }[]
|
||||
}
|
||||
|
||||
type TrialIntermediateValue = {
|
||||
step: number
|
||||
value: TrialIntermediateValueNumber
|
||||
}
|
||||
|
||||
type Distribution =
|
||||
| FloatDistribution
|
||||
| IntDistribution
|
||||
@@ -37,14 +42,18 @@ type Attribute = {
|
||||
value: string
|
||||
}
|
||||
|
||||
type AttributeSpec = {
|
||||
key: string
|
||||
sortable: boolean
|
||||
}
|
||||
|
||||
type Study = {
|
||||
study_id: number
|
||||
study_name: string
|
||||
directions: StudyDirection[]
|
||||
user_attrs: Attribute[]
|
||||
union_search_space: SearchSpaceItem[]
|
||||
intersection_search_space: SearchSpaceItem[]
|
||||
system_attrs: Attribute[]
|
||||
union_user_attrs: AttributeSpec[]
|
||||
datetime_start?: Date
|
||||
trials: Trial[]
|
||||
}
|
||||
@@ -57,15 +66,17 @@ type Trial = {
|
||||
values?: TrialValueNumber[]
|
||||
params: TrialParam[]
|
||||
intermediate_values: TrialIntermediateValue[]
|
||||
user_attrs: Attribute[]
|
||||
datetime_start?: Date
|
||||
datetime_complete?: Date
|
||||
user_attrs: Attribute[]
|
||||
system_attrs: Attribute[]
|
||||
}
|
||||
|
||||
type TrialParam = {
|
||||
name: string
|
||||
param_internal_value: number
|
||||
param_external_value: string
|
||||
param_external_type: string
|
||||
distribution: Distribution
|
||||
}
|
||||
|
||||
type SearchSpaceItem = {
|
||||
|
||||
Reference in New Issue
Block a user