From 357b88f531e75cb5781ad7dcf7eb1c8708d60033 Mon Sep 17 00:00:00 2001 From: Contramundum Date: Fri, 29 Sep 2023 18:14:56 +0900 Subject: [PATCH 001/176] Add close icon to modals --- .../ts/components/PreferenceHistory.tsx | 10 ++++++++++ .../ts/components/ThreejsArtifactViewer.tsx | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/optuna_dashboard/ts/components/PreferenceHistory.tsx b/optuna_dashboard/ts/components/PreferenceHistory.tsx index 6aa67317..018916ae 100644 --- a/optuna_dashboard/ts/components/PreferenceHistory.tsx +++ b/optuna_dashboard/ts/components/PreferenceHistory.tsx @@ -147,6 +147,16 @@ const CandidateTrial: FC<{ overflow: "auto", }} > + setDetailShown(false)} + > + + false} diff --git a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx index 3c4e2e01..e611ae25 100644 --- a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx +++ b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx @@ -6,6 +6,8 @@ import { STLLoader } from "three/examples/jsm/loaders/STLLoader" import { Rhino3dmLoader } from "three/examples/jsm/loaders/3DMLoader" import { PerspectiveCamera } from "three" import { Modal, Box } from "@mui/material" +import ClearIcon from "@mui/icons-material/Clear" +import IconButton from "@mui/material/IconButton" export const isThreejsArtifact = (artifact: Artifact): boolean => { return ( @@ -146,6 +148,19 @@ export const useThreejsArtifactModal = (): [ borderRadius: "15px", }} > + { + setOpen(false) + setTarget(["", null]) + }} + > + + Date: Fri, 29 Sep 2023 18:22:53 +0900 Subject: [PATCH 002/176] format --- .../ts/components/PreferenceHistory.tsx | 14 ++++++------- .../ts/components/ThreejsArtifactViewer.tsx | 20 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/optuna_dashboard/ts/components/PreferenceHistory.tsx b/optuna_dashboard/ts/components/PreferenceHistory.tsx index 018916ae..b7bd04b4 100644 --- a/optuna_dashboard/ts/components/PreferenceHistory.tsx +++ b/optuna_dashboard/ts/components/PreferenceHistory.tsx @@ -148,13 +148,13 @@ const CandidateTrial: FC<{ }} > setDetailShown(false)} - > + sx={{ + position: "absolute", + top: theme.spacing(2), + right: theme.spacing(2), + }} + onClick={() => setDetailShown(false)} + > { - setOpen(false) - setTarget(["", null]) - }} - > + sx={{ + position: "absolute", + top: theme.spacing(2), + right: theme.spacing(2), + }} + onClick={() => { + setOpen(false) + setTarget(["", null]) + }} + > Date: Sat, 30 Sep 2023 17:37:25 +0900 Subject: [PATCH 003/176] Return Result from wasm_fanova_calculate --- rustlib/src/lib.rs | 21 ++++++++++++------- .../src/components/PlotImportance.tsx | 1 + 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/rustlib/src/lib.rs b/rustlib/src/lib.rs index ff09ef81..c326e503 100644 --- a/rustlib/src/lib.rs +++ b/rustlib/src/lib.rs @@ -1,16 +1,20 @@ +use fanova::{FanovaOptions, RandomForestOptions}; use js_sys::Array; use serde_wasm_bindgen::from_value; -use fanova::{FanovaOptions, RandomForestOptions}; use wasm_bindgen::prelude::*; #[wasm_bindgen] -pub fn wasm_fanova_calculate(features: Array, targets: Array) -> Vec { - // TODO(c-bata): Fix error handling +pub fn wasm_fanova_calculate(features: Array, targets: Array) -> Result, JsError> { let features_vec: Vec> = features .iter() - .map(|x| from_value::>(x).unwrap()) - .collect(); - let targets_vec: Vec = targets.iter().map(|x| x.as_f64().unwrap()).collect(); + .map(|x| from_value::>(x)) + .collect::>() + .map_err(|_| JsError::new("features must be of type number[][]"))?; + let targets_vec: Vec = targets + .iter() + .map(|x| x.as_f64()) + .collect::>() + .ok_or(JsError::new("targets must be of type number[]"))?; let mut fanova = FanovaOptions::new() .random_forest(RandomForestOptions::new().seed(0)) @@ -18,9 +22,10 @@ pub fn wasm_fanova_calculate(features: Array, targets: Array) -> Vec { features_vec.iter().map(|x| x.as_slice()).collect(), &targets_vec, ) - .unwrap(); + .map_err(|e| JsError::new(&format!("failed to build fANOVA model: {}", e)))?; let importances = (0..features_vec.len()) .map(|i| fanova.quantify_importance(&[i]).mean) .collect::>(); - return importances; + + Ok(importances) } diff --git a/standalone_app/src/components/PlotImportance.tsx b/standalone_app/src/components/PlotImportance.tsx index 24e9cd8f..7303b2db 100644 --- a/standalone_app/src/components/PlotImportance.tsx +++ b/standalone_app/src/components/PlotImportance.tsx @@ -39,6 +39,7 @@ export const PlotImportance: FC<{ study: Study }> = ({ study }) => { const values = filteredTrials.map( (t) => t.values?.[objectiveId] as number ) + // TODO: handle errors thrown by wasm_fanova_calculate const importance = wasm_fanova_calculate(features, values) return study.intersection_search_space.map((s, i) => ({ name: s.name, From 679646bacf7a949269d0f16a49de305201597e25 Mon Sep 17 00:00:00 2001 From: RuTiO2le Date: Sat, 30 Sep 2023 17:47:35 +0900 Subject: [PATCH 004/176] add csv download --- optuna_dashboard/_app.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index 812201ad..7f263dfd 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -1,6 +1,8 @@ from __future__ import annotations +import csv import functools +import io import logging import os import typing @@ -447,6 +449,41 @@ def create_app( note.save_note_with_version(storage, study_id, trial_id, req_note_ver, req_note_body) response.status = 204 # No content return {} + + @app.get("/csv/") + def download_csv(study_id: int) -> BottleViewReturn: + # TODO: Create a CSV file + summary = get_study_summary(storage, study_id) + if summary is None: + response.status = 404 # Not found + return {"reason": f"study_id={study_id} is not found"} + trials = get_trials(storage, study_id) + + param_names = list(trials[0].params.keys()) + union_user_attrs = list(trials[0].user_attrs) + column_names = ["Number", "State", "Value"] + param_names + union_user_attrs + + buf = io.StringIO("") + writer = csv.writer(buf) + writer.writerow(column_names) + for frozen_trial in trials: + row = [ + frozen_trial.number, + frozen_trial.state, + frozen_trial.values[0] + ] + row += [frozen_trial.params[param] for param in param_names] + row += [frozen_trial.user_attrs[attr] for attr in union_user_attrs] + writer.writerow(row) + + # TODO: Set response headers + response.headers["Content-Type"] = "text/csv; chatset=cp932" + response.headers["Content-Disposition"] = f"attachment; filename=trials_{study_id}.csv" + + # TODO: Response body + buf.seek(0) + return buf.read() + @app.get("/favicon.ico") def favicon() -> BottleViewReturn: From 7dcdce08ddc1c4c2372755586f1d9bd749e15f94 Mon Sep 17 00:00:00 2001 From: Contramundum Date: Thu, 5 Oct 2023 11:09:28 +0900 Subject: [PATCH 005/176] Fix error --- optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx index a4e89756..2a4249d8 100644 --- a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx +++ b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx @@ -5,7 +5,7 @@ import { GizmoHelper, GizmoViewport, OrbitControls } from "@react-three/drei" import { STLLoader } from "three/examples/jsm/loaders/STLLoader" import { Rhino3dmLoader } from "three/examples/jsm/loaders/3DMLoader" import { PerspectiveCamera } from "three" -import { Modal, Box } from "@mui/material" +import { Modal, Box, useTheme } from "@mui/material" import ClearIcon from "@mui/icons-material/Clear" import IconButton from "@mui/material/IconButton" @@ -123,6 +123,7 @@ export const useThreejsArtifactModal = (): [ ] => { const [open, setOpen] = useState(false) const [target, setTarget] = useState<[string, Artifact | null]>(["", null]) + const theme = useTheme() const openModal = (artifactUrlPath: string, artifact: Artifact) => { setTarget([artifactUrlPath, artifact]) From 366ac26e968507d57b94274b51fe58383dc62d32 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Thu, 5 Oct 2023 15:46:52 +0900 Subject: [PATCH 006/176] Add PreferentialGPSampler document --- docs/api.rst | 1 + optuna_dashboard/preferential/samplers/gp.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 18f8bc72..e26dcfd6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -44,6 +44,7 @@ Preferential Optimization optuna_dashboard.preferential.create_study optuna_dashboard.preferential.load_study optuna_dashboard.preferential.PreferentialStudy + optuna_dashboard.preferential.samplers.gp.PreferentialGPSampler optuna_dashboard.register_preference_feedback_component Streamlit diff --git a/optuna_dashboard/preferential/samplers/gp.py b/optuna_dashboard/preferential/samplers/gp.py index cb03fb8f..b1234991 100644 --- a/optuna_dashboard/preferential/samplers/gp.py +++ b/optuna_dashboard/preferential/samplers/gp.py @@ -280,6 +280,25 @@ class _PreferentialGP: class PreferentialGPSampler(optuna.samplers.BaseSampler): + """Sampler for preferential optimization using Gaussian process. + + The sampling algorithm is based on `Takeno et al., 2023 `_. + This sampler uses BoTorch to optimize acquisition function. + + Args: + kernel: + Kernel that computes the covariance on the Gaussian process. + noise_prior: + Prior of the observation noise. + independent_sampler: + A :class:`~optuna.samplers.BaseSampler` instance that is used for independent + sampling. The parameters not contained in the relative search space are sampled + by this sampler. If :obj:`None` is specified, + :class:`~optuna.samplers.RandomSampler` is used as the default. + seed: + Seed for random number generator. + """ + def __init__( self, *, From 09fa32b9141e97aef875f2cc610880af288da778 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Thu, 5 Oct 2023 17:10:43 +0900 Subject: [PATCH 007/176] Install BoTorch on document build --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4eff6f1f..9d8500dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dynamic = ["version"] [project.optional-dependencies] docs = [ "boto3", + "botorch", "streamlit", "sphinx", "sphinx_rtd_theme", From a5e102d9fa250a2f431981ddc7b5f2ae39b811cf Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Thu, 5 Oct 2023 16:40:32 +0900 Subject: [PATCH 008/176] Make PreferentialGPSampler the default for preferential optimization --- optuna_dashboard/preferential/_study.py | 38 +++++++++++++++---------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/optuna_dashboard/preferential/_study.py b/optuna_dashboard/preferential/_study.py index 6e093683..a14ef364 100644 --- a/optuna_dashboard/preferential/_study.py +++ b/optuna_dashboard/preferential/_study.py @@ -7,9 +7,9 @@ from typing import Iterable import optuna from optuna import logging +from optuna._imports import try_import from optuna.distributions import BaseDistribution from optuna.samplers import BaseSampler -from optuna.samplers import RandomSampler from optuna.trial import FrozenTrial from optuna.trial import TrialState from optuna_dashboard.preferential._system_attrs import get_n_generate @@ -20,6 +20,10 @@ from optuna_dashboard.preferential._system_attrs import report_preferences from optuna_dashboard.preferential._system_attrs import set_n_generate +with try_import() as _imports: + from optuna_dashboard.preferential.samplers.gp import PreferentialGPSampler + + _logger = logging.get_logger(__name__) _SYSTEM_ATTR_PREFERENTIAL_STUDY = "preference:is_preferential" @@ -340,11 +344,10 @@ def create_study( sampler: A sampler object that implements background algorithm for value suggestion. - If :obj:`None` is specified, `RandomSampler`_ is used. Please note that - most Optuna samplers does not work efficiently for preferential optimization. - - .. _RandomSampler: https://optuna.readthedocs.io/en/stable/reference/\ - samplers/generated/optuna.samplers.RandomSampler.html + If :obj:`None` is specified, + :class:`~optuna_dashboard.preferential.samplers.gp.PreferentialGPSampler` is used. + Please note that most Optuna samplers does not work efficiently for preferential + optimization. study_name: Study's name. If this argument is set to None, a unique name is generated @@ -361,9 +364,13 @@ def create_study( A :class:`~optuna_dashboard.preferential.PreferentialStudy` object. """ try: + if sampler is None: + _imports.check() # If BoTorch is not installed, raise ImportError. + sampler = PreferentialGPSampler() + study = optuna.create_study( storage=storage, - sampler=sampler or RandomSampler(), + sampler=sampler, study_name=study_name, ) study._storage.set_study_system_attr( @@ -433,18 +440,19 @@ def load_study( :func:`~optuna.study.create_study` for further details. sampler: A sampler object that implements background algorithm for value suggestion. - If :obj:`None` is specified, `RandomSampler`_ is used. Please note that - most Optuna samplers does not work efficiently for preferential optimization. - - .. _RandomSampler: https://optuna.readthedocs.io/en/stable/reference/samplers/\ - generated/optuna.samplers.RandomSampler.html + If :obj:`None` is specified, + :class:`~optuna_dashboard.preferential.samplers.gp.PreferentialGPSampler` is used. + Please note that most Optuna samplers does not work efficiently for preferential + optimization. Returns: A :class:`~optuna_dashboard.preferential.PreferentialStudy` object. """ - study = optuna.load_study( - study_name=study_name, storage=storage, sampler=sampler or RandomSampler() - ) + if sampler is None: + _imports.check() # If BoTorch is not installed, raise ImportError. + sampler = PreferentialGPSampler() + + study = optuna.load_study(study_name=study_name, storage=storage, sampler=sampler) system_attrs = study._storage.get_study_system_attrs(study._study_id) if not system_attrs.get(_SYSTEM_ATTR_PREFERENTIAL_STUDY): raise ValueError("The study is not a PreferentialStudy.") From c6f495dfa694720ba3edb5d81298249bb0556d51 Mon Sep 17 00:00:00 2001 From: RuTiO2le Date: Fri, 6 Oct 2023 06:22:46 +0900 Subject: [PATCH 009/176] add csv download button --- optuna_dashboard/ts/components/StudyDetail.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/optuna_dashboard/ts/components/StudyDetail.tsx b/optuna_dashboard/ts/components/StudyDetail.tsx index ab37d02b..dc7d5894 100644 --- a/optuna_dashboard/ts/components/StudyDetail.tsx +++ b/optuna_dashboard/ts/components/StudyDetail.tsx @@ -12,6 +12,7 @@ import { import Grid2 from "@mui/material/Unstable_Grid2" import ChevronRightIcon from "@mui/icons-material/ChevronRight" import HomeIcon from "@mui/icons-material/Home" +import DownloadIcon from "@mui/icons-material/Download" import { StudyNote } from "./Note" import { actionCreator } from "../action" @@ -143,6 +144,16 @@ export const StudyDetail: FC<{ content = ( + + + From 44ad9489829ed3adee2a5cc89ab1e026906b9a14 Mon Sep 17 00:00:00 2001 From: RuTiO2le Date: Fri, 6 Oct 2023 06:54:02 +0900 Subject: [PATCH 010/176] remove branks in L484 --- optuna_dashboard/_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index 7f263dfd..0a84c7c5 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -481,7 +481,7 @@ def create_app( response.headers["Content-Disposition"] = f"attachment; filename=trials_{study_id}.csv" # TODO: Response body - buf.seek(0) + buf.seek(0) return buf.read() From 20091d2616083a8c96b8086ce4316f549078b4a3 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Fri, 6 Oct 2023 13:47:47 +0900 Subject: [PATCH 011/176] Apply suggestions from code review Co-authored-by: contramundum53 --- optuna_dashboard/preferential/samplers/gp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/preferential/samplers/gp.py b/optuna_dashboard/preferential/samplers/gp.py index b1234991..5edf6454 100644 --- a/optuna_dashboard/preferential/samplers/gp.py +++ b/optuna_dashboard/preferential/samplers/gp.py @@ -287,9 +287,9 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler): Args: kernel: - Kernel that computes the covariance on the Gaussian process. + Kernel that computes the covariance on the Gaussian process. Defaults to Matern 3/2 Kernel + ARD. noise_prior: - Prior of the observation noise. + Prior of the observation noise. Defaults to gamma prior. independent_sampler: A :class:`~optuna.samplers.BaseSampler` instance that is used for independent sampling. The parameters not contained in the relative search space are sampled From bef3d1e57e4d612883a1db8ce6a7df8293ca2c63 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Fri, 6 Oct 2023 13:54:40 +0900 Subject: [PATCH 012/176] Fix lint --- optuna_dashboard/preferential/samplers/gp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/optuna_dashboard/preferential/samplers/gp.py b/optuna_dashboard/preferential/samplers/gp.py index 5edf6454..7815a365 100644 --- a/optuna_dashboard/preferential/samplers/gp.py +++ b/optuna_dashboard/preferential/samplers/gp.py @@ -287,7 +287,8 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler): Args: kernel: - Kernel that computes the covariance on the Gaussian process. Defaults to Matern 3/2 Kernel + ARD. + Kernel that computes the covariance on the Gaussian process. Defaults to + Matern 3/2 Kernel + ARD. noise_prior: Prior of the observation noise. Defaults to gamma prior. independent_sampler: From 6943ae51bf919e86c6eeeca05cae6a8dcb1565a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 05:06:20 +0000 Subject: [PATCH 013/176] Bump postcss from 8.4.29 to 8.4.31 Bumps [postcss](https://github.com/postcss/postcss) from 8.4.29 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.29...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index a3f13d65..886c4bda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12511,9 +12511,9 @@ "integrity": "sha512-2b7w4CQI06px8HVpKpgZtfuoDjuCLA26VlgdnG71UDBrJvtCYvXb39H4ElNv+CA1bbD4S98KanpWPRqTqlxBZw==" }, "node_modules/postcss": { - "version": "8.4.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", - "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -24278,9 +24278,9 @@ "integrity": "sha512-2b7w4CQI06px8HVpKpgZtfuoDjuCLA26VlgdnG71UDBrJvtCYvXb39H4ElNv+CA1bbD4S98KanpWPRqTqlxBZw==" }, "postcss": { - "version": "8.4.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", - "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "requires": { "nanoid": "^3.3.6", From ce4f45538b8a08d143cfebe6e139c9022bc67bc3 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Fri, 6 Oct 2023 14:17:21 +0900 Subject: [PATCH 014/176] Add optional-dependencies for preferential optimization --- docs/tutorials/preferential-optimization.rst | 2 +- pyproject.toml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/preferential-optimization.rst b/docs/tutorials/preferential-optimization.rst index 4d2644ea..a192e883 100644 --- a/docs/tutorials/preferential-optimization.rst +++ b/docs/tutorials/preferential-optimization.rst @@ -27,7 +27,7 @@ First, ensure the necessary packages are installed by executing the following co .. code-block:: console - $ pip install "optuna>=3.3.0" "optuna-dashboard>=0.13.0b1" pillow botorch + $ pip install "optuna>=3.3.0" "optuna-dashboard[preferential]>=0.13.0b1" pillow Next, execute the Python script, copied from `generator.py`_. diff --git a/pyproject.toml b/pyproject.toml index 4eff6f1f..050a624a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,9 @@ optional = [ "botorch", ] +preferential = [ + "botorch>=0.8.1", +] [project.scripts] optuna-dashboard = "optuna_dashboard._cli:main" From 4a683090ff9780a8af3b946fb773e414dfbf4243 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Fri, 6 Oct 2023 15:09:19 +0900 Subject: [PATCH 015/176] Add version constraint for BoTorch --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 050a624a..fd1dcdcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ test = [ optional = [ "streamlit", "boto3", - "botorch", + "botorch>=0.8.1", ] preferential = [ From 07d2bf7946dfe10d572abf38533ac52a9feeeeda Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Fri, 6 Oct 2023 17:54:49 +0900 Subject: [PATCH 016/176] Skip test with Python 3.7 --- python_tests/preferential/test_study.py | 5 +++++ python_tests/test_api.py | 9 +++++++++ python_tests/test_preferential_history.py | 6 ++++++ python_tests/test_serializers.py | 5 +++++ 4 files changed, 25 insertions(+) diff --git a/python_tests/preferential/test_study.py b/python_tests/preferential/test_study.py index 7fc1aaba..9b4a3f31 100644 --- a/python_tests/preferential/test_study.py +++ b/python_tests/preferential/test_study.py @@ -3,6 +3,7 @@ from __future__ import annotations import copy import multiprocessing import pickle +import sys from typing import Callable from unittest.mock import patch import uuid @@ -22,6 +23,10 @@ from ..storage_supplier import parametrize_storages from ..storage_supplier import StorageSupplier +if sys.version_info < (3, 8): + pytest.skip("BoTorch dropped Python3.7 support", allow_module_level=True) + + @parametrize_storages def test_study_set_and_get_user_attrs(storage_supplier: Callable[[], StorageSupplier]) -> None: with storage_supplier() as storage: diff --git a/python_tests/test_api.py b/python_tests/test_api.py index f9c56e4f..bde7838a 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import sys from unittest import TestCase import optuna @@ -14,6 +15,7 @@ from optuna_dashboard._preferential_history import remove_history from optuna_dashboard._preferential_history import report_history from optuna_dashboard._serializer import serialize_preference_history from optuna_dashboard.preferential import create_study +import pytest from .wsgi_client import send_request @@ -105,6 +107,7 @@ class APITestCase(TestCase): ) self.assertEqual(status, 400) + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_get_best_trials_of_preferential_study(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(n_generate=4, storage=storage) @@ -128,6 +131,7 @@ class APITestCase(TestCase): assert len(best_trials) == 1 assert best_trials[0]["number"] == 0 + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_report_preference(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(n_generate=4, storage=storage) @@ -161,6 +165,7 @@ class APITestCase(TestCase): assert better.number == 2 assert worse.number == 1 + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_report_preference_when_typo_mode(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(storage=storage, n_generate=3) @@ -184,6 +189,7 @@ class APITestCase(TestCase): ) self.assertEqual(status, 400) + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_change_component(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(storage=storage, n_generate=3) @@ -214,6 +220,7 @@ class APITestCase(TestCase): assert study_detail["feedback_component_type"]["output_type"] == "artifact" assert study_detail["feedback_component_type"]["artifact_key"] == "image" + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_skip_trial(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(n_generate=4, storage=storage) @@ -238,6 +245,7 @@ class APITestCase(TestCase): assert len(best_trials) == 1 assert best_trials[0].number == 2 + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_remove_history(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(storage=storage, n_generate=3) @@ -271,6 +279,7 @@ class APITestCase(TestCase): assert histories[0]["is_removed"] assert len(study.get_preferences()) == 0 + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_restore_history(self) -> None: storage = optuna.storages.InMemoryStorage() study = create_study(storage=storage, n_generate=3) diff --git a/python_tests/test_preferential_history.py b/python_tests/test_preferential_history.py index 3d1a7d70..dcae4797 100644 --- a/python_tests/test_preferential_history.py +++ b/python_tests/test_preferential_history.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import sys from typing import Callable from typing import TYPE_CHECKING @@ -13,6 +14,7 @@ from optuna_dashboard._preferential_history import restore_history from optuna_dashboard._serializer import serialize_preference_history from optuna_dashboard.preferential import create_study from optuna_dashboard.preferential._system_attrs import _SYSTEM_ATTR_PREFIX_PREFERENCE +import pytest from .storage_supplier import parametrize_storages from .storage_supplier import StorageSupplier @@ -22,6 +24,10 @@ if TYPE_CHECKING: from optuna_dashboard._preferential_history import History +if sys.version_info < (3, 8): + pytest.skip("BoTorch dropped Python3.7 support", allow_module_level=True) + + @parametrize_storages def test_report_and_get_choices(storage_supplier: Callable[[], StorageSupplier]) -> None: with storage_supplier() as storage: diff --git a/python_tests/test_serializers.py b/python_tests/test_serializers.py index ea1c3517..0d1546d2 100644 --- a/python_tests/test_serializers.py +++ b/python_tests/test_serializers.py @@ -1,11 +1,14 @@ from __future__ import annotations +import sys + import optuna from optuna_dashboard._serializer import serialize_attrs from optuna_dashboard._serializer import serialize_study_detail from optuna_dashboard._serializer import serialize_study_summary from optuna_dashboard._storage import get_study_summaries from optuna_dashboard.preferential import create_study +import pytest def test_serialize_bytes() -> None: @@ -22,6 +25,7 @@ def test_serialize_dict() -> None: assert len(serialized) <= 1 +@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_get_study_detail_is_preferential() -> None: storage = optuna.storages.InMemoryStorage() study = create_study(n_generate=4, storage=storage) @@ -48,6 +52,7 @@ def test_get_study_detail_is_not_preferential() -> None: assert not study_detail["is_preferential"] +@pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_get_study_summary_is_preferential() -> None: storage = optuna.storages.InMemoryStorage() create_study(n_generate=4, storage=storage) From 478ef923472b47cd064c9e0356a2451cf3829df5 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Tue, 10 Oct 2023 21:53:11 +0900 Subject: [PATCH 017/176] Add test standalone --- e2e_tests/test_server.py | 26 ++++++++++++++++++++ e2e_tests/test_standalone/__init__.py | 0 e2e_tests/test_standalone/test_study_list.py | 22 +++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 e2e_tests/test_standalone/__init__.py create mode 100644 e2e_tests/test_standalone/test_study_list.py diff --git a/e2e_tests/test_server.py b/e2e_tests/test_server.py index b6d14734..cc41d73a 100644 --- a/e2e_tests/test_server.py +++ b/e2e_tests/test_server.py @@ -1,5 +1,7 @@ import socket import threading +import http.server +import socketserver from wsgiref.simple_server import make_server import optuna @@ -33,3 +35,27 @@ def make_test_server( request.addfinalizer(stop_server) return f"http://{addr}:{port}/dashboard" + + +def make_standalone_server(request: pytest.FixtureRequest) -> str: + addr = "127.0.0.1" + port = get_free_port() + directory = "/Users/k-umezawa/dev/optuna-dashboard/standalone_app/" + + Handler = http.server.SimpleHTTPRequestHandler + httpd = socketserver.TCPServer(("", port), lambda *args, **kwargs: Handler(*args, directory=directory, **kwargs)) + + def serve_httpd(): + httpd.serve_forever() + + thread = threading.Thread(target=serve_httpd) + thread.start() + + def stop_server() -> None: + httpd.shutdown() + httpd.server_close() + thread.join() + + request.addfinalizer(stop_server) + + return f"http://{addr}:{port}" diff --git a/e2e_tests/test_standalone/__init__.py b/e2e_tests/test_standalone/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/e2e_tests/test_standalone/test_study_list.py b/e2e_tests/test_standalone/test_study_list.py new file mode 100644 index 00000000..c2cbcb75 --- /dev/null +++ b/e2e_tests/test_standalone/test_study_list.py @@ -0,0 +1,22 @@ +from playwright.sync_api import Page +import pytest + +from ..test_server import make_standalone_server + + +@pytest.fixture +def server_url(request: pytest.FixtureRequest) -> str: + return make_standalone_server(request) + + +def test_home( + page: Page, + server_url: str, +) -> None: + url = f"{server_url}" + page.goto(url) + element = page.get_by_role("heading") + assert element is not None + title = element.text_content() + assert title is not None + assert title == "Optuna Dashboard (Wasm ver.)" \ No newline at end of file From 102dec04a34cdaceabda64ffa36f7ea4034eb323 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Tue, 10 Oct 2023 22:02:22 +0900 Subject: [PATCH 018/176] Add github actions --- ...{e2e-tests.yml => e2e-dashboard-tests.yml} | 4 +- .github/workflows/e2e-standalone-tests.yml | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) rename .github/workflows/{e2e-tests.yml => e2e-dashboard-tests.yml} (94%) create mode 100644 .github/workflows/e2e-standalone-tests.yml diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-dashboard-tests.yml similarity index 94% rename from .github/workflows/e2e-tests.yml rename to .github/workflows/e2e-dashboard-tests.yml index 4665f676..cb2ed8cf 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-dashboard-tests.yml @@ -1,10 +1,10 @@ -name: e2e-tests +name: e2e-dashboard-tests on: pull_request: branches: - main paths: - - '.github/workflows/e2e-tests.yml' + - '.github/workflows/e2e-dashboard-tests.yml' - '**.py' - '**.ts' - '**.tsx' diff --git a/.github/workflows/e2e-standalone-tests.yml b/.github/workflows/e2e-standalone-tests.yml new file mode 100644 index 00000000..7dceb059 --- /dev/null +++ b/.github/workflows/e2e-standalone-tests.yml @@ -0,0 +1,43 @@ +name: e2e-standalone-tests +on: + pull_request: + branches: + - main + paths: + - '.github/workflows/e2e-standalone-tests.yml' + - '**.py' + - '**.ts' + - '**.tsx' + - 'package.json' + - 'package-lock.json' + - 'tsconfig.json' +jobs: + test: + runs-on: ubuntu-20.04 + strategy: + matrix: + optuna-version: ['optuna==2.10.0', 'git+https://github.com/optuna/optuna.git'] + steps: + - uses: actions/checkout@v3 + + - name: Install Rust toolchains + uses: dtolnay/rust-toolchain@stable + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: '18' + + - name: Build standalone_app + env: + PUBLIC_PATH: /optuna-dashboard/public/ + run: make MODE="prd" standalone_app/public/bundle.js + + - name: Install the required browsers + run: playwright install + + - name: Run e2e tests + run: pytest e2e_tests/test_standalone From aabfaefdec2bd7508e63d8143fd2901cc3727cb7 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Tue, 10 Oct 2023 22:04:38 +0900 Subject: [PATCH 019/176] Move dashboard e2e test files --- e2e_tests/{ => test_dashboard}/test_usecases/__init__.py | 0 .../{ => test_dashboard}/test_usecases/test_study_history.py | 0 e2e_tests/{ => test_dashboard}/visual_regression_test.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename e2e_tests/{ => test_dashboard}/test_usecases/__init__.py (100%) rename e2e_tests/{ => test_dashboard}/test_usecases/test_study_history.py (100%) rename e2e_tests/{ => test_dashboard}/visual_regression_test.py (100%) diff --git a/e2e_tests/test_usecases/__init__.py b/e2e_tests/test_dashboard/test_usecases/__init__.py similarity index 100% rename from e2e_tests/test_usecases/__init__.py rename to e2e_tests/test_dashboard/test_usecases/__init__.py diff --git a/e2e_tests/test_usecases/test_study_history.py b/e2e_tests/test_dashboard/test_usecases/test_study_history.py similarity index 100% rename from e2e_tests/test_usecases/test_study_history.py rename to e2e_tests/test_dashboard/test_usecases/test_study_history.py diff --git a/e2e_tests/visual_regression_test.py b/e2e_tests/test_dashboard/visual_regression_test.py similarity index 100% rename from e2e_tests/visual_regression_test.py rename to e2e_tests/test_dashboard/visual_regression_test.py From 2a94f59451486a8fd1b4abfc7e88ef8231fe4caa Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Tue, 10 Oct 2023 22:09:49 +0900 Subject: [PATCH 020/176] Fix path --- .github/workflows/e2e-dashboard-tests.yml | 2 +- e2e_tests/test_dashboard/test_usecases/test_study_history.py | 2 +- e2e_tests/test_dashboard/visual_regression_test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-dashboard-tests.yml b/.github/workflows/e2e-dashboard-tests.yml index cb2ed8cf..c8912b9a 100644 --- a/.github/workflows/e2e-dashboard-tests.yml +++ b/.github/workflows/e2e-dashboard-tests.yml @@ -47,4 +47,4 @@ jobs: run: playwright install - name: Run e2e tests - run: pytest e2e_tests + run: pytest e2e_tests/test_dashboard diff --git a/e2e_tests/test_dashboard/test_usecases/test_study_history.py b/e2e_tests/test_dashboard/test_usecases/test_study_history.py index e37b8f5a..6c22b16b 100644 --- a/e2e_tests/test_dashboard/test_usecases/test_study_history.py +++ b/e2e_tests/test_dashboard/test_usecases/test_study_history.py @@ -2,7 +2,7 @@ import optuna from playwright.sync_api import Page import pytest -from ..test_server import make_test_server +from ...test_server import make_test_server def make_test_storage() -> optuna.storages.InMemoryStorage: diff --git a/e2e_tests/test_dashboard/visual_regression_test.py b/e2e_tests/test_dashboard/visual_regression_test.py index d83fbb5f..ea6ff930 100644 --- a/e2e_tests/test_dashboard/visual_regression_test.py +++ b/e2e_tests/test_dashboard/visual_regression_test.py @@ -4,7 +4,7 @@ import optuna from playwright.sync_api import Page import pytest -from .test_server import make_test_server +from ..test_server import make_test_server @pytest.fixture From 3e6a2bfa4453f7e7231d626e8dd277ac5b2c4a4b Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Tue, 10 Oct 2023 22:33:24 +0900 Subject: [PATCH 021/176] Fix some errors --- .github/workflows/e2e-standalone-tests.yml | 10 ++++++++++ e2e_tests/test_dashboard/__init__.py | 0 2 files changed, 10 insertions(+) create mode 100644 e2e_tests/test_dashboard/__init__.py diff --git a/.github/workflows/e2e-standalone-tests.yml b/.github/workflows/e2e-standalone-tests.yml index 7dceb059..2187982b 100644 --- a/.github/workflows/e2e-standalone-tests.yml +++ b/.github/workflows/e2e-standalone-tests.yml @@ -31,6 +31,16 @@ jobs: with: node-version: '18' + - name: Setup Optuna ${{ matrix.optuna-version }} + run: | + python -m pip install --progress-bar off --upgrade pip setuptools + python -m pip install --progress-bar off --upgrade ${{ matrix.optuna-version }} + + - name: Install dependencies + run: | + python -m pip install --progress-bar off . + python -m pip install --progress-bar off pytest-playwright + - name: Build standalone_app env: PUBLIC_PATH: /optuna-dashboard/public/ diff --git a/e2e_tests/test_dashboard/__init__.py b/e2e_tests/test_dashboard/__init__.py new file mode 100644 index 00000000..e69de29b From 3ab4be4ce318f31a8b998a89054b35393f6e7f3d Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Tue, 10 Oct 2023 22:34:24 +0900 Subject: [PATCH 022/176] Fix lint --- e2e_tests/test_server.py | 8 +++++--- e2e_tests/test_standalone/test_study_list.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/e2e_tests/test_server.py b/e2e_tests/test_server.py index cc41d73a..89886181 100644 --- a/e2e_tests/test_server.py +++ b/e2e_tests/test_server.py @@ -1,7 +1,7 @@ -import socket -import threading import http.server +import socket import socketserver +import threading from wsgiref.simple_server import make_server import optuna @@ -43,7 +43,9 @@ def make_standalone_server(request: pytest.FixtureRequest) -> str: directory = "/Users/k-umezawa/dev/optuna-dashboard/standalone_app/" Handler = http.server.SimpleHTTPRequestHandler - httpd = socketserver.TCPServer(("", port), lambda *args, **kwargs: Handler(*args, directory=directory, **kwargs)) + httpd = socketserver.TCPServer( + ("", port), lambda *args, **kwargs: Handler(*args, directory=directory, **kwargs) + ) def serve_httpd(): httpd.serve_forever() diff --git a/e2e_tests/test_standalone/test_study_list.py b/e2e_tests/test_standalone/test_study_list.py index c2cbcb75..b0d4735e 100644 --- a/e2e_tests/test_standalone/test_study_list.py +++ b/e2e_tests/test_standalone/test_study_list.py @@ -19,4 +19,4 @@ def test_home( assert element is not None title = element.text_content() assert title is not None - assert title == "Optuna Dashboard (Wasm ver.)" \ No newline at end of file + assert title == "Optuna Dashboard (Wasm ver.)" From 6f4724f73346a63decee69b4bdd549c6dbef6f6a Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Tue, 10 Oct 2023 23:00:03 +0900 Subject: [PATCH 023/176] Fix public path --- .github/workflows/e2e-standalone-tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/e2e-standalone-tests.yml b/.github/workflows/e2e-standalone-tests.yml index 2187982b..2de6d610 100644 --- a/.github/workflows/e2e-standalone-tests.yml +++ b/.github/workflows/e2e-standalone-tests.yml @@ -42,8 +42,6 @@ jobs: python -m pip install --progress-bar off pytest-playwright - name: Build standalone_app - env: - PUBLIC_PATH: /optuna-dashboard/public/ run: make MODE="prd" standalone_app/public/bundle.js - name: Install the required browsers From ba259e622201dd84c4dc0b3190487ccdc9b3cf3e Mon Sep 17 00:00:00 2001 From: c-bata Date: Thu, 12 Oct 2023 14:28:24 +0900 Subject: [PATCH 024/176] Bump the version up to VS Code Extension 0.1.0 --- vscode/README.md | 4 ++++ vscode/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/vscode/README.md b/vscode/README.md index 78587c37..960669a3 100644 --- a/vscode/README.md +++ b/vscode/README.md @@ -14,6 +14,10 @@ Nothing to configure. ## Release Notes +### 0.1.0 + +Added older database schemas support (Optuna 2.6.0 or later) + ### 0.0.1 Initial Release diff --git a/vscode/package.json b/vscode/package.json index a5c1d818..6a99a905 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -3,7 +3,7 @@ "displayName": "Optuna Dashboard", "description": "Web Dashboard for Optuna", "publisher": "Optuna", - "version": "0.0.1", + "version": "0.1.0", "license": "MIT", "icon": "images/optuna-logo.png", "engines": { From ea3a986915f743b49c9ee887bf4ca2a1691816a8 Mon Sep 17 00:00:00 2001 From: HideakiImamura Date: Thu, 12 Oct 2023 15:46:53 +0900 Subject: [PATCH 025/176] Add rank plot --- optuna_dashboard/ts/components/GraphRank.tsx | 343 ++++++++++++++++++ .../ts/components/StudyDetail.tsx | 6 + 2 files changed, 349 insertions(+) create mode 100644 optuna_dashboard/ts/components/GraphRank.tsx diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx new file mode 100644 index 00000000..17731101 --- /dev/null +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -0,0 +1,343 @@ +import * as plotly from "plotly.js-dist-min" +import React, { FC, useEffect, useState } from "react" +import { + Grid, + FormControl, + FormLabel, + MenuItem, + Select, + Typography, + SelectChangeEvent, + useTheme, + Box, +} from "@mui/material" +import { plotlyDarkTemplate } from "./PlotlyDarkMode" +import { makeHovertext } from "../graphUtil" +import { useMergedUnionSearchSpace } from "../searchSpace" + +const PADDING_RATIO = 0.05 +const plotDomId = "graph-rank" + +interface AxisInfo { + name: string + range: [number, number] + is_log: boolean + is_cat: boolean +} + +interface RankPlotInfo { + xaxis: AxisInfo + yaxis: AxisInfo + xvalues: (string | number)[] + yvalues: (string | number)[] + zvalues: number[] + colors: number[] + hovertext: string[] +} + +export const GraphRank: FC<{ + study: StudyDetail | null +}> = ({ study = null }) => { + const theme = useTheme() + const [objectiveId, setobjectiveId] = useState(0) + const searchSpace = useMergedUnionSearchSpace(study?.union_search_space) + const [xParam, setXParam] = useState(null) + const [yParam, setYParam] = useState(null) + const objectiveNames: string[] = study?.objective_names || [] + + if (xParam == null && searchSpace.length > 0) { + setXParam(searchSpace[0]) + } + if (yParam == null && searchSpace.length > 1) { + setYParam(searchSpace[1]) + } + + const handleObjectiveChange = (event: SelectChangeEvent) => { + setobjectiveId(Number(event.target.value)) + } + const handleXParamChange = (event: SelectChangeEvent) => { + const param = searchSpace.find((item) => item.name === event.target.value) + setXParam(param || null) + } + const handleYParamChange = (event: SelectChangeEvent) => { + const param = searchSpace.find((item) => item.name === event.target.value) + setYParam(param || null) + } + + const rankPlotInfo = getRankPlotInfo(study, objectiveId, xParam, yParam) + + useEffect(() => { + if (study != null) { + plotRank(rankPlotInfo, theme.palette.mode) + } + }, [study, theme.palette.mode]) + + const space: SearchSpaceItem[] = study ? study.union_search_space : [] + + return ( + + + + Rank + + {study !== null && study.directions.length !== 1 ? ( + + Objective: + + + ) : null} + {study !== null && space.length > 0 ? ( + + + x: + + + + y: + + + + ) : null} + + + + + + ) +} + +const getRankPlotInfo = ( + study: StudyDetail | null, + objectiveId: number, + xParam: SearchSpaceItem | null, + yParam: SearchSpaceItem | null +): RankPlotInfo | null => { + if (study === null) { + return null + } + + const trials = study.trials + const filtered_trials = trials.filter(filterFunc) + if (filtered_trials.length < 2 || xParam == null || yParam == null) { + return null + } + + const xAxis = getAxisInfo(filtered_trials, xParam) + const yAxis = getAxisInfo(filtered_trials, yParam) + + const xValues: number[] = [] + const yValues: number[] = [] + const zValues: number[] = [] + const hovertext: string[] = [] + filtered_trials.forEach((trial) => { + const xValue = + trial.params.find((p) => p.name === xAxis.name)?.param_internal_value || + null + const yValue = + trial.params.find((p) => p.name === yAxis.name)?.param_internal_value || + null + if (trial.values === undefined || xValue === null || yValue === null) { + return + } + const zValue = Number(trial.values[objectiveId]) + xValues.push(xValue) + yValues.push(yValue) + zValues.push(zValue) + hovertext.push(makeHovertext(trial)) + }) + + const colors = getColors(zValues) + + return { + xaxis: xAxis, + yaxis: yAxis, + xvalues: xValues, + yvalues: yValues, + zvalues: zValues, + colors, + hovertext, + } +} + +const filterFunc = (trial: Trial): boolean => { + return trial.state === "Complete" && trial.values !== undefined +} + +const getAxisInfo = (trials: Trial[], param: SearchSpaceItem): AxisInfo => { + if (param.distribution.type === "CategoricalDistribution") { + return getAxisInfoForCategorical(trials, param.name, param.distribution) + } else { + return getAxisInfoForNumerical(trials, param.name, param.distribution) + } +} + +const getAxisInfoForCategorical = ( + trials: Trial[], + param: string, + distribution: CategoricalDistribution +): AxisInfo => { + const values = trials.map( + (trial) => + trial.params.find((p) => p.name === param)?.param_internal_value || null + ) + const isDynamic = values.some((v) => v === null) + const span = distribution.choices.length - (isDynamic ? 2 : 1) + const padding = span * PADDING_RATIO + const min = -padding + const max = span + padding + + return { + name: param, + range: [min, max], + is_log: false, + is_cat: true, + } +} + +const getAxisInfoForNumerical = ( + trials: Trial[], + param: string, + distribution: FloatDistribution | IntDistribution +): AxisInfo => { + const values = trials.map( + (trial) => + trial.params.find((p) => p.name === param)?.param_internal_value || null + ) + const non_null_values: number[] = [] + values.forEach((value) => { + if (value !== null) { + non_null_values.push(value) + } + }) + let min = Math.min(...non_null_values) + let max = Math.max(...non_null_values) + if (distribution.log) { + const padding = (Math.log10(max) - Math.log10(min)) * PADDING_RATIO + min = Math.pow(10, Math.log10(min) - padding) + max = Math.pow(10, Math.log10(max) + padding) + } else { + const padding = (max - min) * PADDING_RATIO + min = min - padding + max = max + padding + } + + return { + name: param, + range: [min, max], + is_log: distribution.log, + is_cat: false, + } +} + +const getColors = (values: number[]): number[] => { + const raw_ranks = getOrderWithSameOrderAveraging(values) + let color_idxs: number[] = [] + if (values.length > 2) { + color_idxs = raw_ranks.map((rank) => rank / (values.length - 1)) + } else { + color_idxs = [0.5] + } + return color_idxs +} + +const getOrderWithSameOrderAveraging = (values: number[]): number[] => { + const sorted_values = values.slice().sort() + const ranks: number[] = [] + values.forEach((value) => { + const first_index = sorted_values.indexOf(value) + const last_index = sorted_values.lastIndexOf(value) + const sum_of_the_value = sorted_values + .slice(first_index, last_index + 1) + .reduce((a, b) => a + b, 0) + const rank = sum_of_the_value / (last_index - first_index + 1) + ranks.push(rank) + }) + return ranks +} + +const plotRank = (rank_plot_info: RankPlotInfo | null, mode: string) => { + if (document.getElementById(plotDomId) === null) { + return + } + + if (rank_plot_info === null) { + plotly.react(plotDomId, [], { + template: mode === "dark" ? plotlyDarkTemplate : {}, + }) + return + } + + const xAxis = rank_plot_info.xaxis + const yAxis = rank_plot_info.yaxis + const layout: Partial = { + xaxis: { + title: xAxis.name, + type: xAxis.is_cat ? "category" : xAxis.is_log ? "log" : "linear", + }, + yaxis: { + title: yAxis.name, + type: yAxis.is_cat ? "category" : yAxis.is_log ? "log" : "linear", + }, + margin: { + l: 50, + t: 0, + r: 50, + b: 50, + }, + uirevision: "true", + template: mode === "dark" ? plotlyDarkTemplate : {}, + } + const plotData: Partial[] = [ + { + type: "scatter", + x: rank_plot_info.xvalues, + y: rank_plot_info.yvalues, + marker: { + color: rank_plot_info.colors, + colorscale: "Portland", + colorbar: { + title: "Rank", + }, + size: 10, + line: { + color: "Grey", + width: 0.5, + }, + }, + mode: "markers", + showlegend: false, + hovertemplate: "%{hovertext}", + hovertext: rank_plot_info.hovertext, + }, + ] + plotly.react(plotDomId, plotData, layout) +} diff --git a/optuna_dashboard/ts/components/StudyDetail.tsx b/optuna_dashboard/ts/components/StudyDetail.tsx index 1a8ced33..663ff399 100644 --- a/optuna_dashboard/ts/components/StudyDetail.tsx +++ b/optuna_dashboard/ts/components/StudyDetail.tsx @@ -27,6 +27,7 @@ import { GraphParallelCoordinate } from "./GraphParallelCoordinate" import { Contour } from "./GraphContour" import { GraphSlice } from "./GraphSlice" import { GraphEdf } from "./GraphEdf" +import { GraphRank } from "./GraphRank" import { TrialList } from "./TrialList" import { StudyHistory } from "./StudyHistory" import { PreferentialTrials } from "./PreferentialTrials" @@ -121,6 +122,11 @@ export const StudyDetail: FC<{ + + + + + Empirical Distribution of the Objective Value From 7c92b4a731e61332d813b54bfe9d6808494642e4 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Thu, 12 Oct 2023 17:11:08 +0900 Subject: [PATCH 026/176] Add test for _one_side_trunc_norm_sampling --- python_tests/preferential/samplers/test_gp.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 python_tests/preferential/samplers/test_gp.py diff --git a/python_tests/preferential/samplers/test_gp.py b/python_tests/preferential/samplers/test_gp.py new file mode 100644 index 00000000..a249f4b2 --- /dev/null +++ b/python_tests/preferential/samplers/test_gp.py @@ -0,0 +1,29 @@ +import sys +from unittest.mock import patch + +import numpy as np +import pytest +import torch + + +if sys.version_info >= (3, 8): + from optuna_dashboard.preferential.samplers.gp import _one_side_trunc_norm_sampling +else: + pytest.skip("BoTorch dropped Python3.7 support", allow_module_level=True) + + +def test_one_side_trunc_norm_sampling() -> None: + for lower in np.linspace(-10, 10, 100): + assert _one_side_trunc_norm_sampling(torch.tensor([lower], dtype=torch.float64)) >= lower + + with patch.object(torch, "rand", return_value=torch.tensor([0.4], dtype=torch.float64)): + sampled_value = _one_side_trunc_norm_sampling(torch.tensor([0.1], dtype=torch.float64)) + assert np.allclose(sampled_value.numpy(), 0.899967154837563) + + with patch.object(torch, "rand", return_value=torch.tensor([0.8], dtype=torch.float64)): + sampled_value = _one_side_trunc_norm_sampling(torch.tensor([-2.3], dtype=torch.float64)) + assert np.allclose(sampled_value.numpy(), -0.8113606739551955) + + with patch.object(torch, "rand", return_value=torch.tensor([0.1], dtype=torch.float64)): + sampled_value = _one_side_trunc_norm_sampling(torch.tensor([5], dtype=torch.float64)) + assert np.allclose(sampled_value.numpy(), 5.426934003050024) From 926ebd3362813d518342af6999ae82ce047d76a7 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Thu, 12 Oct 2023 17:34:47 +0900 Subject: [PATCH 027/176] Improve accuracy of _one_side_trunc_norm_sampling --- optuna_dashboard/preferential/samplers/gp.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/optuna_dashboard/preferential/samplers/gp.py b/optuna_dashboard/preferential/samplers/gp.py index 2e782b68..457379dc 100644 --- a/optuna_dashboard/preferential/samplers/gp.py +++ b/optuna_dashboard/preferential/samplers/gp.py @@ -50,15 +50,15 @@ def _orthants_MVN_Gibbs_sampling(cov_inv: Tensor, cycles: int, initial_sample: T def _one_side_trunc_norm_sampling(lower: Tensor) -> Tensor: - if lower > 4.0: - r = torch.clamp_min(torch.rand(torch.Size(()), dtype=torch.float64), min=1e-300) - return (lower * lower - 2 * r.log()).sqrt() - else: - SQRT2 = math.sqrt(2) - r = torch.rand(torch.Size(()), dtype=torch.float64) * torch.erfc(lower / SQRT2) - while 1 - r == 1: - r = torch.rand(torch.Size(()), dtype=torch.float64) * torch.erfc(lower / SQRT2) - return torch.erfinv(1 - r) * SQRT2 + r = torch.rand(torch.Size(()), dtype=torch.float64) + ret = -torch.special.ndtri(torch.exp(torch.special.log_ndtr(-lower) + r.log())) + + # If sampled random number is very small, `ret` becomes inf. + while torch.isinf(ret): + r = torch.rand(torch.Size(()), dtype=torch.float64) + ret = -torch.special.ndtri(torch.exp(torch.special.log_ndtr(-lower) + r.log())) + + return ret _orthants_MVN_Gibbs_sampling_jit = torch.jit.script(_orthants_MVN_Gibbs_sampling) From f0888409d2e84e0129ff1c1b4b92779677565f84 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Thu, 12 Oct 2023 18:12:34 +0900 Subject: [PATCH 028/176] Fix python-tests CI with Python 3.7 --- .github/workflows/python-tests.yml | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 8fee4e44..69923d04 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -6,6 +6,7 @@ on: paths: - '.github/workflows/python-tests.yml' - '**.py' + - 'pyproject.toml' jobs: lint: runs-on: ubuntu-latest diff --git a/pyproject.toml b/pyproject.toml index 1477ef4c..1f125735 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ test = [ optional = [ "streamlit", "boto3", - "botorch>=0.8.1", + "botorch>=0.8.1; python_version>='3.8'", ] preferential = [ From 82cc858a7929f1fb16e2200a21590edcf488738e Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Fri, 13 Oct 2023 11:08:51 +0900 Subject: [PATCH 029/176] Fix test for Python 3.7 --- python_tests/preferential/samplers/test_gp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_tests/preferential/samplers/test_gp.py b/python_tests/preferential/samplers/test_gp.py index a249f4b2..136959a7 100644 --- a/python_tests/preferential/samplers/test_gp.py +++ b/python_tests/preferential/samplers/test_gp.py @@ -3,11 +3,11 @@ from unittest.mock import patch import numpy as np import pytest -import torch if sys.version_info >= (3, 8): from optuna_dashboard.preferential.samplers.gp import _one_side_trunc_norm_sampling + import torch else: pytest.skip("BoTorch dropped Python3.7 support", allow_module_level=True) From 3177cd91742651da7784c02c51dd85920c2671d9 Mon Sep 17 00:00:00 2001 From: c-bata Date: Fri, 13 Oct 2023 14:33:17 +0900 Subject: [PATCH 030/176] Bump the version up to v0.13.0 --- optuna_dashboard/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/__init__.py b/optuna_dashboard/__init__.py index ea2a8dbe..8b793e62 100644 --- a/optuna_dashboard/__init__.py +++ b/optuna_dashboard/__init__.py @@ -17,4 +17,4 @@ from ._note import save_note # noqa from ._preference_setting import register_preference_feedback_component # noqa -__version__ = "0.13.0b1" +__version__ = "0.13.0" From 327cf732ca411a14e925b646220baa1417e9c124 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Fri, 13 Oct 2023 11:31:39 +0200 Subject: [PATCH 031/176] [enhance] Change default sorting of study to descending --- optuna_dashboard/ts/components/StudyList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/ts/components/StudyList.tsx b/optuna_dashboard/ts/components/StudyList.tsx index c41c51cc..2191750a 100644 --- a/optuna_dashboard/ts/components/StudyList.tsx +++ b/optuna_dashboard/ts/components/StudyList.tsx @@ -65,7 +65,7 @@ export const StudyList: FC<{ } const query = useQuery() const initialSortBy = - query.get("studies_order_by") === "desc" ? "desc" : "asc" + query.get("studies_order_by") === "asc" ? "asc" : "desc" const [sortBy, setSortBy] = useState<"asc" | "desc">(initialSortBy) let filteredStudies = studies.filter((s) => !studyFilter(s)) From 00d117cfaf2e6b2241bb0ff3bb76bb1166c25436 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Fri, 13 Oct 2023 11:36:34 +0200 Subject: [PATCH 032/176] Format the file to pass the linter --- optuna_dashboard/ts/components/StudyList.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/optuna_dashboard/ts/components/StudyList.tsx b/optuna_dashboard/ts/components/StudyList.tsx index 2191750a..14eaa68b 100644 --- a/optuna_dashboard/ts/components/StudyList.tsx +++ b/optuna_dashboard/ts/components/StudyList.tsx @@ -64,8 +64,7 @@ export const StudyList: FC<{ return useMemo(() => new URLSearchParams(search), [search]) } const query = useQuery() - const initialSortBy = - query.get("studies_order_by") === "asc" ? "asc" : "desc" + const initialSortBy = query.get("studies_order_by") === "asc" ? "asc" : "desc" const [sortBy, setSortBy] = useState<"asc" | "desc">(initialSortBy) let filteredStudies = studies.filter((s) => !studyFilter(s)) From b71a86225bad395f803e905b4f8649776e285395 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Sun, 15 Oct 2023 16:51:17 +0900 Subject: [PATCH 033/176] Enable ssh --- .github/workflows/e2e-standalone-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/e2e-standalone-tests.yml b/.github/workflows/e2e-standalone-tests.yml index 2de6d610..b27246be 100644 --- a/.github/workflows/e2e-standalone-tests.yml +++ b/.github/workflows/e2e-standalone-tests.yml @@ -44,6 +44,9 @@ jobs: - name: Build standalone_app run: make MODE="prd" standalone_app/public/bundle.js + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + - name: Install the required browsers run: playwright install From 844ac81cb2dabcf70b16de3fe094c59cc7332d98 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Sun, 15 Oct 2023 17:08:47 +0900 Subject: [PATCH 034/176] Fix directory path --- .github/workflows/e2e-standalone-tests.yml | 3 --- e2e_tests/test_server.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/e2e-standalone-tests.yml b/.github/workflows/e2e-standalone-tests.yml index b27246be..2de6d610 100644 --- a/.github/workflows/e2e-standalone-tests.yml +++ b/.github/workflows/e2e-standalone-tests.yml @@ -44,9 +44,6 @@ jobs: - name: Build standalone_app run: make MODE="prd" standalone_app/public/bundle.js - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - - name: Install the required browsers run: playwright install diff --git a/e2e_tests/test_server.py b/e2e_tests/test_server.py index 89886181..a15acc68 100644 --- a/e2e_tests/test_server.py +++ b/e2e_tests/test_server.py @@ -40,7 +40,7 @@ def make_test_server( def make_standalone_server(request: pytest.FixtureRequest) -> str: addr = "127.0.0.1" port = get_free_port() - directory = "/Users/k-umezawa/dev/optuna-dashboard/standalone_app/" + directory = "./standalone_app/" Handler = http.server.SimpleHTTPRequestHandler httpd = socketserver.TCPServer( From 4df81f221d98dcc97a996b0adb71696268ea231d Mon Sep 17 00:00:00 2001 From: HideakiImamura Date: Sun, 15 Oct 2023 23:35:01 +0900 Subject: [PATCH 035/176] Fix rank calculation logic --- optuna_dashboard/ts/components/GraphRank.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index 17731101..6030a722 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -275,9 +275,7 @@ const getOrderWithSameOrderAveraging = (values: number[]): number[] => { values.forEach((value) => { const first_index = sorted_values.indexOf(value) const last_index = sorted_values.lastIndexOf(value) - const sum_of_the_value = sorted_values - .slice(first_index, last_index + 1) - .reduce((a, b) => a + b, 0) + const sum_of_the_value = (first_index + last_index) * (last_index - first_index + 1) / 2 const rank = sum_of_the_value / (last_index - first_index + 1) ranks.push(rank) }) From 2e0f64cc80b9ab838d016f5e3b833b0ca48c2e83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 20:52:50 +0000 Subject: [PATCH 036/176] Bump @babel/traverse from 7.20.5 to 7.23.2 Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.20.5 to 7.23.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 244 +++++++++++++++++++++++----------------------- 1 file changed, 124 insertions(+), 120 deletions(-) diff --git a/package-lock.json b/package-lock.json index 886c4bda..0561a75a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,11 +75,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -125,13 +126,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", - "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.20.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -250,9 +252,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" @@ -271,25 +273,25 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -417,29 +419,29 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -483,12 +485,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -496,9 +498,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", - "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1657,33 +1659,33 @@ } }, "node_modules/@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", - "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.5", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.5", - "@babel/types": "^7.20.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1692,12 +1694,12 @@ } }, "node_modules/@babel/types": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", - "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -15313,11 +15315,12 @@ } }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -15350,13 +15353,14 @@ } }, "@babel/generator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", - "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "requires": { - "@babel/types": "^7.20.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { @@ -15444,9 +15448,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-explode-assignable-expression": { @@ -15459,22 +15463,22 @@ } }, "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -15569,23 +15573,23 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { "version": "7.18.6", @@ -15617,19 +15621,19 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", - "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { @@ -16401,41 +16405,41 @@ } }, "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", - "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.5", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.5", - "@babel/types": "^7.20.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", - "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, From 7a3a287f570171c2fae481024cdf6b176862b6cc Mon Sep 17 00:00:00 2001 From: hrntsm Date: Thu, 19 Oct 2023 17:30:29 +0900 Subject: [PATCH 037/176] Fix 3js camera settings --- .../ts/components/ThreejsArtifactViewer.tsx | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx index 2a4249d8..f04a269e 100644 --- a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx +++ b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx @@ -34,7 +34,7 @@ const CustomGizmoHelper: React.FC = () => { ) } -const calculateBoundingBox = (geometries: THREE.BufferGeometry[]) => { +const computeBoundingBox = (geometries: THREE.BufferGeometry[]) => { const boundingBox = new THREE.Box3() geometries.forEach((geometry) => { const mesh = new THREE.Mesh(geometry) @@ -47,8 +47,11 @@ export const ThreejsArtifactViewer: React.FC = ( props ) => { const [geometry, setGeometry] = useState([]) - const [modelSize, setModelSize] = useState( - new THREE.Vector3(10, 10, 10) + const [boundingBox, setBoundingBox] = useState( + new THREE.Box3( + new THREE.Vector3(-10, -10, -10), + new THREE.Vector3(10, 10, 10) + ) ) const [cameraSettings, setCameraSettings] = useState( new THREE.PerspectiveCamera() @@ -56,11 +59,11 @@ export const ThreejsArtifactViewer: React.FC = ( const handleLoadedGeometries = (geometries: THREE.BufferGeometry[]) => { setGeometry(geometries) - const boundingBox = calculateBoundingBox(geometries) + const boundingBox = computeBoundingBox(geometries) if (boundingBox !== null) { - const size = boundingBox.getSize(new THREE.Vector3()) - setModelSize(size) + setBoundingBox(boundingBox) } + return boundingBox } useEffect(() => { @@ -83,20 +86,30 @@ export const ThreejsArtifactViewer: React.FC = ( } }) } - const maxModelSize = Math.max(modelSize.x, modelSize.y, modelSize.z) - const cameraSet = new THREE.PerspectiveCamera( - modelSize - ? Math.min( - 45, - Math.atan(modelSize.y / modelSize.z) * (180 / Math.PI) * 2 - ) - : 45, - window.innerWidth / window.innerHeight - ) - cameraSet.position.set(maxModelSize * 2, maxModelSize * 2, maxModelSize * 2) - setCameraSettings(cameraSet) }, []) + useEffect(() => { + const cameraSet = new THREE.PerspectiveCamera( + 50, + window.innerWidth / window.innerHeight, + 0.1, + boundingBox.getSize(new THREE.Vector3()).length() * 100 + ) + const maxPosition = Math.max( + boundingBox.max.x, + boundingBox.max.y, + boundingBox.max.z + ) + cameraSet.position.set( + maxPosition * 1.5, + maxPosition * 1.5, + maxPosition * 1.5 + ) + const center = boundingBox.getCenter(new THREE.Vector3()) + cameraSet.lookAt(center.x, center.y, center.z) + setCameraSettings(cameraSet) + }, [boundingBox]) + return ( = ( > - + {props.hasGizmo && } {geometry.length > 0 && From 14afdee705130c7dd4e476c0c9b9798674801c84 Mon Sep 17 00:00:00 2001 From: c-bata Date: Thu, 19 Oct 2023 18:47:36 +0900 Subject: [PATCH 038/176] Update README to introduce Jupyter Lab extension --- README.md | 14 +++++++++++++- docs/_static/jupyterlab-extension.png | Bin 0 -> 249747 bytes docs/getting-started.rst | 13 +++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 docs/_static/jupyterlab-extension.png diff --git a/README.md b/README.md index 62edd6c8..badea560 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,18 @@ $ docker run -it --rm -p 8080:8080 ghcr.io/optuna/optuna-dashboard postgresql+ps +## Jupyter Lab Extension (Experimental) + +You can install the Jupyter Lab extension via [PyPI](https://pypi.org/project/jupyterlab-optuna/). + +``` +$ pip install jupyterlab jupyterlab-optuna +``` + +Jupyter Lab Extension + +To use, click the tile to launch the extension, and enter your Optuna’s storage URL (e.g. `sqlite:///db.sqlite3`) in the dialog. + ## Browser-only version (Experimental) Browser-only version @@ -93,7 +105,7 @@ https://optuna.github.io/optuna-dashboard/ You can install the VS Code extension via [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=Optuna.optuna-dashboard#overview). -VSCode Extension +VS Code Extension Please right-click the SQLite3 files (`*.db` or `*.sqlite3`) in the VS Code file explorer and select the "Open in Optuna Dashboard" command from the dropdown menu. This extension leverages the browser-only version of Optuna Dashboard, so the same limitations apply. diff --git a/docs/_static/jupyterlab-extension.png b/docs/_static/jupyterlab-extension.png new file mode 100644 index 0000000000000000000000000000000000000000..7652e87fa2f8cd957e55d0924d3c1bf7d5894fb3 GIT binary patch literal 249747 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
)uM0Z!R^6 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�A^2MQ=zN%ZCn8 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 literal 0 HcmV?d00001 diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 295dd55c..d3b040c6 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -178,6 +178,19 @@ or $ pip install uwsgi $ uwsgi --http :8080 --workeers 4 --wsgi-file wsgi.py +Jupyter Lab Extension (Experimental) +-------------------------------- + +You can install the Jupyter Lab extension via `PyPI `_. + +.. figure:: _static/jupyterlab-extension.png + :alt: Screenshot for the Jupyter Lab Extension + :align: center + :width: 800px + + Jupyter Lab Extension + +To use, click the tile to launch the extension, and enter your Optuna’s storage URL (e.g. ``sqlite:///db.sqlite3``) in the dialog. Browser-only version (Experimental) ----------------------------------- From d39a2a2a63b74aa7939071bed0b0f5c2e783c641 Mon Sep 17 00:00:00 2001 From: HideakiImamura Date: Fri, 20 Oct 2023 10:56:34 +0900 Subject: [PATCH 039/176] Run linter --- optuna_dashboard/ts/components/GraphRank.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index 6030a722..b6e2fd1f 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -275,7 +275,8 @@ const getOrderWithSameOrderAveraging = (values: number[]): number[] => { values.forEach((value) => { const first_index = sorted_values.indexOf(value) const last_index = sorted_values.lastIndexOf(value) - const sum_of_the_value = (first_index + last_index) * (last_index - first_index + 1) / 2 + const sum_of_the_value = + ((first_index + last_index) * (last_index - first_index + 1)) / 2 const rank = sum_of_the_value / (last_index - first_index + 1) ranks.push(rank) }) From 933070b8a1e120f88d7c48165b0ef6f37214eabf Mon Sep 17 00:00:00 2001 From: gen740 Date: Fri, 20 Oct 2023 17:19:52 +0900 Subject: [PATCH 040/176] Implement uplead study artifact ui --- optuna_dashboard/artifact/_backend.py | 35 ++- optuna_dashboard/ts/action.ts | 41 +++- optuna_dashboard/ts/apiClient.ts | 7 +- optuna_dashboard/ts/components/Note.tsx | 4 +- .../ts/components/StudyArtifactCards.tsx | 232 ++++++++++++++++++ .../ts/components/StudyHistory.tsx | 14 ++ .../ts/components/TrialArtifactCards.tsx | 4 +- 7 files changed, 328 insertions(+), 9 deletions(-) create mode 100644 optuna_dashboard/ts/components/StudyArtifactCards.tsx diff --git a/optuna_dashboard/artifact/_backend.py b/optuna_dashboard/artifact/_backend.py index 10a5b1ee..ee1b85a8 100644 --- a/optuna_dashboard/artifact/_backend.py +++ b/optuna_dashboard/artifact/_backend.py @@ -105,7 +105,7 @@ def register_artifact_route( @app.post("/api/artifacts//") @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,6 +144,39 @@ def register_artifact_route( "artifacts": list_trial_artifacts(storage.get_study_system_attrs(study_id), trial), } + @app.post("/api/artifacts/") + @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///") @json_api_view def delete_artifact(study_id: int, trial_id: int, artifact_id: str) -> dict[str, Any]: diff --git a/optuna_dashboard/ts/action.ts b/optuna_dashboard/ts/action.ts index 41afdadb..cfcf4c24 100644 --- a/optuna_dashboard/ts/action.ts +++ b/optuna_dashboard/ts/action.ts @@ -100,6 +100,12 @@ export const actionCreator = () => { setTrial(studyId, trialIndex, newTrial) } + const setStudyArtifacts = (studyId: number, artifacts: Artifact[]) => { + const newStudy: StudyDetail = Object.assign({}, studyDetails[studyId]) + newStudy.artifacts = artifacts + setStudyDetailState(studyId, newStudy) + } + const deleteTrialArtifact = ( studyId: number, trialId: number, @@ -430,7 +436,7 @@ export const actionCreator = () => { }) } - const uploadArtifact = ( + const uploadTrialArtifact = ( studyId: number, trialId: number, file: File @@ -467,6 +473,36 @@ export const actionCreator = () => { } } + const uploadStudyArtifact = ( + studyId: number, + file: File + ): void => { + const reader = new FileReader() + setUploading(true) + reader.readAsDataURL(file) + reader.onload = (upload: ProgressEvent) => { + uploadArtifactAPI( + studyId, + null, + 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 deleteArtifact = ( studyId: number, trialId: number, @@ -693,7 +729,8 @@ export const actionCreator = () => { saveReloadInterval, saveStudyNote, saveTrialNote, - uploadArtifact, + uploadTrialArtifact, + uploadStudyArtifact, deleteArtifact, makeTrialComplete, makeTrialFail, diff --git a/optuna_dashboard/ts/apiClient.ts b/optuna_dashboard/ts/apiClient.ts index e5510d67..c1dedd14 100644 --- a/optuna_dashboard/ts/apiClient.ts +++ b/optuna_dashboard/ts/apiClient.ts @@ -282,12 +282,15 @@ type UploadArtifactAPIResponse = { export const uploadArtifactAPI = ( studyId: number, - trialId: number, + trialId: number | null, fileName: string, dataUrl: string ): Promise => { + const APIurl = `/api/artifacts/${studyId}${ + trialId != null ? `/${trialId}` : "" + }` return axiosInstance - .post(`/api/artifacts/${studyId}/${trialId}`, { + .post(APIurl, { file: dataUrl, filename: fileName, }) diff --git a/optuna_dashboard/ts/components/Note.tsx b/optuna_dashboard/ts/components/Note.tsx index 09270213..586f34e0 100644 --- a/optuna_dashboard/ts/components/Note.tsx +++ b/optuna_dashboard/ts/components/Note.tsx @@ -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) => { diff --git a/optuna_dashboard/ts/components/StudyArtifactCards.tsx b/optuna_dashboard/ts/components/StudyArtifactCards.tsx new file mode 100644 index 00000000..32f36830 --- /dev/null +++ b/optuna_dashboard/ts/components/StudyArtifactCards.tsx @@ -0,0 +1,232 @@ +import React, { + FC, + useState, + DragEventHandler, + useRef, + MouseEventHandler, + ChangeEventHandler, +} from "react" +import { + Typography, + Box, + Card, + useTheme, + CardContent, + CardActionArea, + IconButton, +} from "@mui/material" +import { ArtifactCardMedia } from "./ArtifactCardMedia" +import FullscreenIcon from "@mui/icons-material/Fullscreen" +import UploadFileIcon from "@mui/icons-material/UploadFile" +import DownloadIcon from "@mui/icons-material/Download" +import { actionCreator } from "../action" + +import { + isThreejsArtifact, + useThreejsArtifactModal, +} from "./ThreejsArtifactViewer" + +export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { + const theme = useTheme() + const height = "150px" + const width = "200px" + + const [openThreejsArtifactModal, renderThreejsArtifactModal] = + useThreejsArtifactModal() + + return ( + <> + + Study Artifacts Test + + + + {study.artifacts.map((artifact) => { + const urlPath = `/artifacts/${study.id}/${artifact.artifact_id}` + return ( + + + + + {artifact.filename} + + {isThreejsArtifact(artifact) ? ( + { + openThreejsArtifactModal(urlPath, artifact) + }} + > + + + ) : null} + {/* TODO(gen740): add delete functionality + { + openDeleteArtifactDialog( + trial.study_id, + trial.trial_id, + artifact + ) + }} + > + + */} + + + + + + ) + })} + + + {renderThreejsArtifactModal()} + + ) +} + +const StudyArtifactUploader: FC<{ + study: StudyDetail + width: string + height: string +}> = ({ study, width, height }) => { + const theme = useTheme() + const [dragOver, setDragOver] = useState(false) + const action = actionCreator() + + const inputRef = useRef(null) + const handleClick: MouseEventHandler = () => { + if (!inputRef || !inputRef.current) { + return + } + inputRef.current.click() + } + + const handleOnChange: ChangeEventHandler = (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 ( + + + + + + Upload a New File + + Drag your file here or click to browse. + + + + + ) +} diff --git a/optuna_dashboard/ts/components/StudyHistory.tsx b/optuna_dashboard/ts/components/StudyHistory.tsx index 907acd1a..e89a0ee4 100644 --- a/optuna_dashboard/ts/components/StudyHistory.tsx +++ b/optuna_dashboard/ts/components/StudyHistory.tsx @@ -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(false) const [includePruned, setIncludePruned] = useState(true) + const artifactEnabled = useRecoilValue(artifactIsAvailable) const handleLogScaleChange = () => { setLogScale(!logScale) @@ -167,6 +171,16 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { + + + + + {artifactEnabled && studyDetail !== null && ( + + )} + + + ) } diff --git a/optuna_dashboard/ts/components/TrialArtifactCards.tsx b/optuna_dashboard/ts/components/TrialArtifactCards.tsx index 3e15f7ed..edbbce65 100644 --- a/optuna_dashboard/ts/components/TrialArtifactCards.tsx +++ b/optuna_dashboard/ts/components/TrialArtifactCards.tsx @@ -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) => { From ee36eefcdcd8718ef0deb17c2b85f6b9b0860d7f Mon Sep 17 00:00:00 2001 From: gen740 Date: Fri, 20 Oct 2023 17:29:54 +0900 Subject: [PATCH 041/176] reorder import statements --- .../ts/components/StudyArtifactCards.tsx | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/optuna_dashboard/ts/components/StudyArtifactCards.tsx b/optuna_dashboard/ts/components/StudyArtifactCards.tsx index 32f36830..9d9f35f9 100644 --- a/optuna_dashboard/ts/components/StudyArtifactCards.tsx +++ b/optuna_dashboard/ts/components/StudyArtifactCards.tsx @@ -1,39 +1,43 @@ import React, { - FC, - useState, - DragEventHandler, - useRef, - MouseEventHandler, ChangeEventHandler, + DragEventHandler, + FC, + MouseEventHandler, + useRef, + useState, } from "react" import { Typography, Box, - Card, useTheme, + IconButton, + Card, CardContent, CardActionArea, - IconButton, } from "@mui/material" -import { ArtifactCardMedia } from "./ArtifactCardMedia" -import FullscreenIcon from "@mui/icons-material/Fullscreen" import UploadFileIcon from "@mui/icons-material/UploadFile" import DownloadIcon from "@mui/icons-material/Download" -import { actionCreator } from "../action" +import DeleteIcon from "@mui/icons-material/Delete" +import FullscreenIcon from "@mui/icons-material/Fullscreen" +import { actionCreator } from "../action" +import { useDeleteArtifactDialog } from "./DeleteArtifactDialog" import { - isThreejsArtifact, useThreejsArtifactModal, + isThreejsArtifact, } from "./ThreejsArtifactViewer" +import { ArtifactCardMedia } from "./ArtifactCardMedia" export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { const theme = useTheme() - const height = "150px" - const width = "200px" - + const [openDeleteArtifactDialog, renderDeleteArtifactDialog] = + useDeleteArtifactDialog() const [openThreejsArtifactModal, renderThreejsArtifactModal] = useThreejsArtifactModal() + const width = "200px" + const height = "150px" + return ( <> = ({ study }) => { ) : null} - {/* TODO(gen740): add delete functionality = ({ study }) => { }} > - */} + Date: Fri, 20 Oct 2023 18:04:29 +0900 Subject: [PATCH 042/176] Add delete study artifact api --- optuna_dashboard/artifact/_backend.py | 29 ++++- optuna_dashboard/ts/action.ts | 55 ++++++-- optuna_dashboard/ts/apiClient.ts | 37 +++++- .../ts/components/DeleteArtifactDialog.tsx | 122 +++++++++++++----- .../ts/components/StudyArtifactCards.tsx | 11 +- .../ts/components/TrialArtifactCards.tsx | 4 +- python_tests/artifact/test_backend.py | 2 +- 7 files changed, 195 insertions(+), 65 deletions(-) diff --git a/optuna_dashboard/artifact/_backend.py b/optuna_dashboard/artifact/_backend.py index ee1b85a8..55dc844c 100644 --- a/optuna_dashboard/artifact/_backend.py +++ b/optuna_dashboard/artifact/_backend.py @@ -179,7 +179,7 @@ def register_artifact_route( @app.delete("/api/artifacts///") @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."} @@ -187,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) @@ -196,6 +196,25 @@ def register_artifact_route( response.status = 204 return {} + @app.delete("/api/artifacts//") + @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) + + # The artifact's metadata is stored in one of the following two locations: + storage.set_study_system_attr( + study_id, _dashboard_artifact_prefix(study_id) + artifact_id, json.dumps(None) + ) + storage.set_study_system_attr( + study_id, ARTIFACTS_ATTR_PREFIX + artifact_id, json.dumps(None) + ) + + response.status = 204 + return {} + def upload_artifact( backend: ArtifactBackend, @@ -253,7 +272,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}:" @@ -273,7 +292,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) @@ -317,7 +336,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 diff --git a/optuna_dashboard/ts/action.ts b/optuna_dashboard/ts/action.ts index cfcf4c24..e3be3d85 100644 --- a/optuna_dashboard/ts/action.ts +++ b/optuna_dashboard/ts/action.ts @@ -11,9 +11,11 @@ import { tellTrialAPI, saveTrialUserAttrsAPI, renameStudyAPI, - uploadArtifactAPI, + uploadTrialArtifactAPI, + uploadStudyArtifactAPI, getMetaInfoAPI, - deleteArtifactAPI, + deleteTrialArtifactAPI, + deleteStudyArtifactAPI, reportPreferenceAPI, skipPreferentialTrialAPI, removePreferentialHistoryAPI, @@ -106,7 +108,7 @@ export const actionCreator = () => { setStudyDetailState(studyId, newStudy) } - const deleteTrialArtifact = ( + const deleteTrialArtifactState = ( studyId: number, trialId: number, artifact_id: string @@ -128,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, @@ -445,7 +459,7 @@ export const actionCreator = () => { setUploading(true) reader.readAsDataURL(file) reader.onload = (upload: ProgressEvent) => { - uploadArtifactAPI( + uploadTrialArtifactAPI( studyId, trialId, file.name, @@ -473,17 +487,13 @@ export const actionCreator = () => { } } - const uploadStudyArtifact = ( - studyId: number, - file: File - ): void => { + const uploadStudyArtifact = (studyId: number, file: File): void => { const reader = new FileReader() setUploading(true) reader.readAsDataURL(file) reader.onload = (upload: ProgressEvent) => { - uploadArtifactAPI( + uploadStudyArtifactAPI( studyId, - null, file.name, upload.target?.result as string ) @@ -503,14 +513,30 @@ export const actionCreator = () => { } } - const deleteArtifact = ( + 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", }) @@ -731,7 +757,8 @@ export const actionCreator = () => { saveTrialNote, uploadTrialArtifact, uploadStudyArtifact, - deleteArtifact, + deleteTrialArtifact, + deleteStudyArtifact, makeTrialComplete, makeTrialFail, saveTrialUserAttrs, diff --git a/optuna_dashboard/ts/apiClient.ts b/optuna_dashboard/ts/apiClient.ts index c1dedd14..f42d20de 100644 --- a/optuna_dashboard/ts/apiClient.ts +++ b/optuna_dashboard/ts/apiClient.ts @@ -280,17 +280,14 @@ type UploadArtifactAPIResponse = { artifacts: Artifact[] } -export const uploadArtifactAPI = ( +export const uploadTrialArtifactAPI = ( studyId: number, - trialId: number | null, + trialId: number, fileName: string, dataUrl: string ): Promise => { - const APIurl = `/api/artifacts/${studyId}${ - trialId != null ? `/${trialId}` : "" - }` return axiosInstance - .post(APIurl, { + .post(`/api/artifacts/${studyId}/${trialId}`, { file: dataUrl, filename: fileName, }) @@ -299,7 +296,22 @@ export const uploadArtifactAPI = ( }) } -export const deleteArtifactAPI = ( +export const uploadStudyArtifactAPI = ( + studyId: number, + fileName: string, + dataUrl: string +): Promise => { + return axiosInstance + .post(`/api/artifacts/${studyId}`, { + file: dataUrl, + filename: fileName, + }) + .then((res) => { + return res.data + }) +} + +export const deleteTrialArtifactAPI = ( studyId: number, trialId: number, artifactId: string @@ -311,6 +323,17 @@ export const deleteArtifactAPI = ( }) } +export const deleteStudyArtifactAPI = ( + studyId: number, + artifactId: string +): Promise => { + return axiosInstance + .delete(`/api/artifacts/${studyId}/${artifactId}`) + .then(() => { + return + }) +} + export const tellTrialAPI = ( trialId: number, state: TrialStateFinished, diff --git a/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx b/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx index 2c9c229e..4bf5e95d 100644 --- a/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx +++ b/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState } from "react" +import React, { ReactNode, useState, FC } from "react" import { Dialog, DialogTitle, @@ -9,9 +9,9 @@ import { } from "@mui/material" import { actionCreator } from "../action" -export const useDeleteArtifactDialog = (): [ +export const useDeleteTrialArtifactDialog = (): [ (studyId: number, trialId: number, artifact: Artifact) => void, - () => ReactNode + () => ReactNode, ] => { const action = actionCreator() @@ -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 ( - { - handleCloseDeleteArtifactDialog() - }} - aria-labelledby="delete-artifact-dialog-title" - > - - Delete artifact - - - - Are you sure you want to delete an artifact (" - {target[2]?.filename}")? - - - - - - - + ) } 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 ( + + ) + } + return [openDialog, renderDeleteArtifactDialog] +} + +const DeleteDialog: FC<{ + openDeleteArtifactDialog: boolean + handleCloseDeleteArtifactDialog: () => void + filename: string | undefined + handleDeleteArtifact: () => void +}> = ({ + openDeleteArtifactDialog, + handleCloseDeleteArtifactDialog, + filename, + handleDeleteArtifact, +}) => { + return ( + { + handleCloseDeleteArtifactDialog() + }} + aria-labelledby="delete-artifact-dialog-title" + > + + Delete artifact + + + + Are you sure you want to delete an artifact (" + {filename}")? + + + + + + + + ) +} diff --git a/optuna_dashboard/ts/components/StudyArtifactCards.tsx b/optuna_dashboard/ts/components/StudyArtifactCards.tsx index 9d9f35f9..71143ed4 100644 --- a/optuna_dashboard/ts/components/StudyArtifactCards.tsx +++ b/optuna_dashboard/ts/components/StudyArtifactCards.tsx @@ -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 { useDeleteStudyArtifactDialog } from "./DeleteArtifactDialog" import { useThreejsArtifactModal, isThreejsArtifact, @@ -31,7 +31,7 @@ import { ArtifactCardMedia } from "./ArtifactCardMedia" export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { const theme = useTheme() const [openDeleteArtifactDialog, renderDeleteArtifactDialog] = - useDeleteArtifactDialog() + useDeleteStudyArtifactDialog() const [openThreejsArtifactModal, renderThreejsArtifactModal] = useThreejsArtifactModal() @@ -104,11 +104,7 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { color="inherit" sx={{ margin: "auto 0" }} onClick={() => { - openDeleteArtifactDialog( - trial.study_id, - trial.trial_id, - artifact - ) + openDeleteArtifactDialog(study.id, artifact) }} > @@ -129,6 +125,7 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { })} + {renderDeleteArtifactDialog()} {renderThreejsArtifactModal()} ) diff --git a/optuna_dashboard/ts/components/TrialArtifactCards.tsx b/optuna_dashboard/ts/components/TrialArtifactCards.tsx index edbbce65..1a078f29 100644 --- a/optuna_dashboard/ts/components/TrialArtifactCards.tsx +++ b/optuna_dashboard/ts/components/TrialArtifactCards.tsx @@ -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() diff --git a/python_tests/artifact/test_backend.py b/python_tests/artifact/test_backend.py index 74698a09..22544e6f 100644 --- a/python_tests/artifact/test_backend.py +++ b/python_tests/artifact/test_backend.py @@ -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:" From df13d1b0802c47d8f2440508d042f16408c1626b Mon Sep 17 00:00:00 2001 From: Victoria A <52001888+adjeiv@users.noreply.github.com> Date: Sat, 21 Oct 2023 17:13:46 +0100 Subject: [PATCH 043/176] Use ascending param to decide how to order infinite values --- optuna_dashboard/ts/components/DataGrid.tsx | 5 +++-- optuna_dashboard/ts/components/TrialTable.tsx | 16 ++++++++-------- standalone_app/src/components/DataGrid.tsx | 5 +++-- standalone_app/src/components/TrialTable.tsx | 16 ++++++++-------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/optuna_dashboard/ts/components/DataGrid.tsx b/optuna_dashboard/ts/components/DataGrid.tsx index 2d6a5670..d19ec9d9 100644 --- a/optuna_dashboard/ts/components/DataGrid.tsx +++ b/optuna_dashboard/ts/components/DataGrid.tsx @@ -28,7 +28,7 @@ interface DataGridColumn { field: keyof T label: string sortable?: boolean - less?: (a: T, b: T) => number + less?: (a: T, b: T, ascending: boolean) => number filterable?: boolean toCellValue?: (rowIndex: number) => string | React.ReactNode padding?: "normal" | "checkbox" | "none" @@ -358,7 +358,8 @@ function stableSort( const stabilizedThis = array.map((el, index) => [el, index] as [T, number]) stabilizedThis.sort((a, b) => { if (less) { - const result = order == "asc" ? -less(a[0], b[0]) : less(a[0], b[0]) + const ascending = order == "asc" + const result = ascending ? -less(a[0], b[0], ascending) : less(a[0], b[0], ascending) if (result !== 0) return result } else { const result = comparator(a[0], b[0]) diff --git a/optuna_dashboard/ts/components/TrialTable.tsx b/optuna_dashboard/ts/components/TrialTable.tsx index 8fcce63c..6090ee85 100644 --- a/optuna_dashboard/ts/components/TrialTable.tsx +++ b/optuna_dashboard/ts/components/TrialTable.tsx @@ -28,7 +28,7 @@ export const TrialTable: FC<{ field: "values", label: "Value", sortable: true, - less: (firstEl, secondEl): number => { + less: (firstEl, secondEl, ascending): number => { const firstVal = firstEl.values?.[0] const secondVal = secondEl.values?.[0] @@ -36,9 +36,9 @@ export const TrialTable: FC<{ return 0 } if (firstVal === undefined) { - return -1 + return ascending ? -1 : 1 } else if (secondVal === undefined) { - return 1 + return ascending ? 1 : -1 } if (firstVal === "-inf" || secondVal === "inf") { return 1 @@ -63,7 +63,7 @@ export const TrialTable: FC<{ ? objectiveNames[objectiveId] : `Objective ${objectiveId}`, sortable: true, - less: (firstEl, secondEl): number => { + less: (firstEl, secondEl, ascending): number => { const firstVal = firstEl.values?.[objectiveId] const secondVal = secondEl.values?.[objectiveId] @@ -71,9 +71,9 @@ export const TrialTable: FC<{ return 0 } if (firstVal === undefined) { - return -1 + return ascending ? -1 : 1 } else if (secondVal === undefined) { - return 1 + return ascending ? 1 : -1 } if (firstVal === "-inf" || secondVal === "inf") { return 1 @@ -106,7 +106,7 @@ export const TrialTable: FC<{ ?.param_external_value || null, sortable: sortable, filterable: filterable, - less: (firstEl, secondEl): number => { + less: (firstEl, secondEl, _): number => { const firstVal = firstEl.params.find( (p) => p.name === s.name )?.param_internal_value @@ -146,7 +146,7 @@ export const TrialTable: FC<{ ?.value || null, sortable: attr_spec.sortable, filterable: !attr_spec.sortable, - less: (firstEl, secondEl): number => { + less: (firstEl, secondEl, _): number => { const firstVal = firstEl.user_attrs.find( (attr) => attr.key === attr_spec.key )?.value diff --git a/standalone_app/src/components/DataGrid.tsx b/standalone_app/src/components/DataGrid.tsx index 2d6a5670..d19ec9d9 100644 --- a/standalone_app/src/components/DataGrid.tsx +++ b/standalone_app/src/components/DataGrid.tsx @@ -28,7 +28,7 @@ interface DataGridColumn { field: keyof T label: string sortable?: boolean - less?: (a: T, b: T) => number + less?: (a: T, b: T, ascending: boolean) => number filterable?: boolean toCellValue?: (rowIndex: number) => string | React.ReactNode padding?: "normal" | "checkbox" | "none" @@ -358,7 +358,8 @@ function stableSort( const stabilizedThis = array.map((el, index) => [el, index] as [T, number]) stabilizedThis.sort((a, b) => { if (less) { - const result = order == "asc" ? -less(a[0], b[0]) : less(a[0], b[0]) + const ascending = order == "asc" + const result = ascending ? -less(a[0], b[0], ascending) : less(a[0], b[0], ascending) if (result !== 0) return result } else { const result = comparator(a[0], b[0]) diff --git a/standalone_app/src/components/TrialTable.tsx b/standalone_app/src/components/TrialTable.tsx index 58d2d101..9e890c83 100644 --- a/standalone_app/src/components/TrialTable.tsx +++ b/standalone_app/src/components/TrialTable.tsx @@ -25,7 +25,7 @@ export const TrialTable: FC<{ field: "values", label: "Value", sortable: true, - less: (firstEl, secondEl): number => { + less: (firstEl, secondEl, ascending): number => { const firstVal = firstEl.values?.[0] const secondVal = secondEl.values?.[0] @@ -33,9 +33,9 @@ export const TrialTable: FC<{ return 0 } if (firstVal === undefined) { - return -1 + return ascending ? -1 : 1 } else if (secondVal === undefined) { - return 1 + return ascending ? 1 : -1 } if (firstVal === "-inf" || secondVal === "inf") { return 1 @@ -57,7 +57,7 @@ export const TrialTable: FC<{ field: "values", label: `Objective ${objectiveId}`, sortable: true, - less: (firstEl, secondEl): number => { + less: (firstEl, secondEl, ascending): number => { const firstVal = firstEl.values?.[objectiveId] const secondVal = secondEl.values?.[objectiveId] @@ -65,9 +65,9 @@ export const TrialTable: FC<{ return 0 } if (firstVal === undefined) { - return -1 + return ascending ? -1 : 1 } else if (secondVal === undefined) { - return 1 + return ascending ? 1 : -1 } if (firstVal === "-inf" || secondVal === "inf") { return 1 @@ -96,7 +96,7 @@ export const TrialTable: FC<{ null, sortable: true, filterable: false, - less: (firstEl, secondEl): number => { + less: (firstEl, secondEl, _): number => { const firstVal = firstEl.params.find( (p) => p.name === s.name )?.param_internal_value @@ -126,7 +126,7 @@ export const TrialTable: FC<{ ?.value || null, sortable: attr_spec.sortable, filterable: false, - less: (firstEl, secondEl): number => { + less: (firstEl, secondEl, _): number => { const firstVal = firstEl.user_attrs.find( (attr) => attr.key === attr_spec.key )?.value From 44c398ec36671d30ac5fd54d6b12206b1f99028f Mon Sep 17 00:00:00 2001 From: hrntsm Date: Mon, 23 Oct 2023 18:21:48 +0900 Subject: [PATCH 044/176] Add 3jsArtifactViewer to OBJLoader --- .../ts/components/ThreejsArtifactViewer.tsx | 67 ++++++++++++++----- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx index f04a269e..8b970702 100644 --- a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx +++ b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx @@ -4,6 +4,7 @@ import { Canvas } from "@react-three/fiber" import { GizmoHelper, GizmoViewport, OrbitControls } from "@react-three/drei" import { STLLoader } from "three/examples/jsm/loaders/STLLoader" import { Rhino3dmLoader } from "three/examples/jsm/loaders/3DMLoader" +import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader" import { PerspectiveCamera } from "three" import { Modal, Box, useTheme } from "@mui/material" import ClearIcon from "@mui/icons-material/Clear" @@ -11,7 +12,9 @@ import IconButton from "@mui/material/IconButton" export const isThreejsArtifact = (artifact: Artifact): boolean => { return ( - artifact.filename.endsWith(".stl") || artifact.filename.endsWith(".3dm") + artifact.filename.endsWith(".stl") || + artifact.filename.endsWith(".3dm") || + artifact.filename.endsWith(".obj") ) } @@ -68,23 +71,11 @@ export const ThreejsArtifactViewer: React.FC = ( useEffect(() => { if ("stl" === props.filetype) { - const stlLoader = new STLLoader() - stlLoader.load(props.src, (stlGeometries: THREE.BufferGeometry) => { - if (stlGeometries) { - handleLoadedGeometries([stlGeometries]) - } - }) + loadSTL(props, handleLoadedGeometries) } else if ("3dm" === props.filetype) { - const loader = new Rhino3dmLoader() - loader.setLibraryPath("https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/") - loader.load(props.src, (object: THREE.Object3D) => { - const meshes = object.children as THREE.Mesh[] - const rhinoGeometries = meshes.map((mesh) => mesh.geometry) - THREE.Object3D.DEFAULT_UP.set(0, 0, 1) - if (rhinoGeometries.length > 0) { - handleLoadedGeometries(rhinoGeometries) - } - }) + loadRhino3dm(props, handleLoadedGeometries) + } else if ("obj" === props.filetype) { + loadOBJ(props, handleLoadedGeometries) } }, []) @@ -188,3 +179,45 @@ export const useThreejsArtifactModal = (): [ } return [openModal, renderDeleteStudyDialog] } + +function loadSTL( + props: ThreejsArtifactViewerProps, + handleLoadedGeometries: (geometries: THREE.BufferGeometry[]) => THREE.Box3 +) { + const stlLoader = new STLLoader() + stlLoader.load(props.src, (stlGeometries: THREE.BufferGeometry) => { + if (stlGeometries) { + handleLoadedGeometries([stlGeometries]) + } + }) +} + +function loadRhino3dm( + props: ThreejsArtifactViewerProps, + handleLoadedGeometries: (geometries: THREE.BufferGeometry[]) => THREE.Box3 +) { + const loader = new Rhino3dmLoader() + loader.setLibraryPath("https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/") + loader.load(props.src, (object: THREE.Object3D) => { + const meshes = object.children as THREE.Mesh[] + const rhinoGeometries = meshes.map((mesh) => mesh.geometry) + THREE.Object3D.DEFAULT_UP.set(0, 0, 1) + if (rhinoGeometries.length > 0) { + handleLoadedGeometries(rhinoGeometries) + } + }) +} + +function loadOBJ( + props: ThreejsArtifactViewerProps, + handleLoadedGeometries: (geometries: THREE.BufferGeometry[]) => THREE.Box3 +) { + const objLoader = new OBJLoader() + objLoader.load(props.src, (object: THREE.Object3D) => { + const meshes = object.children as THREE.Mesh[] + const objGeometries = meshes.map((mesh) => mesh.geometry) + if (objGeometries.length > 0) { + handleLoadedGeometries(objGeometries) + } + }) +} From 9ba21c20ba16cbefefb19304397bed7f83954d8d Mon Sep 17 00:00:00 2001 From: hrntsm Date: Mon, 23 Oct 2023 18:38:23 +0900 Subject: [PATCH 045/176] Add Canvas to frameloop to improve performance --- optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx index 8b970702..7baf983f 100644 --- a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx +++ b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx @@ -103,6 +103,7 @@ export const ThreejsArtifactViewer: React.FC = ( return ( From 67d08cfbc0d48c9e71b3052553e5911b592c5948 Mon Sep 17 00:00:00 2001 From: hrntsm Date: Mon, 23 Oct 2023 18:41:53 +0900 Subject: [PATCH 046/176] Update 3dm loader name --- optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx index 7baf983f..8d8c045d 100644 --- a/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx +++ b/optuna_dashboard/ts/components/ThreejsArtifactViewer.tsx @@ -197,9 +197,9 @@ function loadRhino3dm( props: ThreejsArtifactViewerProps, handleLoadedGeometries: (geometries: THREE.BufferGeometry[]) => THREE.Box3 ) { - const loader = new Rhino3dmLoader() - loader.setLibraryPath("https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/") - loader.load(props.src, (object: THREE.Object3D) => { + const rhino3dmLoader = new Rhino3dmLoader() + rhino3dmLoader.setLibraryPath("https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/") + rhino3dmLoader.load(props.src, (object: THREE.Object3D) => { const meshes = object.children as THREE.Mesh[] const rhinoGeometries = meshes.map((mesh) => mesh.geometry) THREE.Object3D.DEFAULT_UP.set(0, 0, 1) From a4ff5c61afd4e98cd94b0d94b5620940c07446c2 Mon Sep 17 00:00:00 2001 From: Victoria A <52001888+adjeiv@users.noreply.github.com> Date: Wed, 25 Oct 2023 08:14:02 +0100 Subject: [PATCH 047/176] Suppress no-unused-vars --- optuna_dashboard/ts/components/TrialTable.tsx | 2 ++ standalone_app/src/components/TrialTable.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/optuna_dashboard/ts/components/TrialTable.tsx b/optuna_dashboard/ts/components/TrialTable.tsx index 6090ee85..c708f6ee 100644 --- a/optuna_dashboard/ts/components/TrialTable.tsx +++ b/optuna_dashboard/ts/components/TrialTable.tsx @@ -106,6 +106,7 @@ export const TrialTable: FC<{ ?.param_external_value || null, sortable: sortable, filterable: filterable, + // eslint-disable-next-line @typescript-eslint/no-unused-vars less: (firstEl, secondEl, _): number => { const firstVal = firstEl.params.find( (p) => p.name === s.name @@ -146,6 +147,7 @@ export const TrialTable: FC<{ ?.value || null, sortable: attr_spec.sortable, filterable: !attr_spec.sortable, + // eslint-disable-next-line @typescript-eslint/no-unused-vars less: (firstEl, secondEl, _): number => { const firstVal = firstEl.user_attrs.find( (attr) => attr.key === attr_spec.key diff --git a/standalone_app/src/components/TrialTable.tsx b/standalone_app/src/components/TrialTable.tsx index 9e890c83..5a0efc96 100644 --- a/standalone_app/src/components/TrialTable.tsx +++ b/standalone_app/src/components/TrialTable.tsx @@ -96,6 +96,7 @@ export const TrialTable: FC<{ null, sortable: true, filterable: false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars less: (firstEl, secondEl, _): number => { const firstVal = firstEl.params.find( (p) => p.name === s.name @@ -126,6 +127,7 @@ export const TrialTable: FC<{ ?.value || null, sortable: attr_spec.sortable, filterable: false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars less: (firstEl, secondEl, _): number => { const firstVal = firstEl.user_attrs.find( (attr) => attr.key === attr_spec.key From 4719e7df98feb7ea08dabf871ef469936c0e7872 Mon Sep 17 00:00:00 2001 From: HideakiImamura Date: Wed, 25 Oct 2023 18:13:04 +0900 Subject: [PATCH 048/176] Follow review comments --- optuna_dashboard/ts/components/GraphRank.tsx | 85 ++++++++++---------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index b6e2fd1f..553612de 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -21,8 +21,8 @@ const plotDomId = "graph-rank" interface AxisInfo { name: string range: [number, number] - is_log: boolean - is_cat: boolean + isLog: boolean + isCat: boolean } interface RankPlotInfo { @@ -45,10 +45,10 @@ export const GraphRank: FC<{ const [yParam, setYParam] = useState(null) const objectiveNames: string[] = study?.objective_names || [] - if (xParam == null && searchSpace.length > 0) { + if (xParam === null && searchSpace.length > 0) { setXParam(searchSpace[0]) } - if (yParam == null && searchSpace.length > 1) { + if (yParam === null && searchSpace.length > 1) { setYParam(searchSpace[1]) } @@ -64,13 +64,12 @@ export const GraphRank: FC<{ setYParam(param || null) } - const rankPlotInfo = getRankPlotInfo(study, objectiveId, xParam, yParam) - useEffect(() => { if (study != null) { + const rankPlotInfo = getRankPlotInfo(study, objectiveId, xParam, yParam) plotRank(rankPlotInfo, theme.palette.mode) } - }, [study, theme.palette.mode]) + }, [study, objectiveId, xParam, yParam, theme.palette.mode]) const space: SearchSpaceItem[] = study ? study.union_search_space : [] @@ -146,19 +145,19 @@ const getRankPlotInfo = ( } const trials = study.trials - const filtered_trials = trials.filter(filterFunc) - if (filtered_trials.length < 2 || xParam == null || yParam == null) { + const filteredTrials = trials.filter(filterFunc) + if (filteredTrials.length < 2 || xParam === null || yParam === null) { return null } - const xAxis = getAxisInfo(filtered_trials, xParam) - const yAxis = getAxisInfo(filtered_trials, yParam) + const xAxis = getAxisInfo(filteredTrials, xParam) + const yAxis = getAxisInfo(filteredTrials, yParam) - const xValues: number[] = [] - const yValues: number[] = [] + const xValues: (string | number)[] = [] + const yValues: (string | number)[] = [] const zValues: number[] = [] const hovertext: string[] = [] - filtered_trials.forEach((trial) => { + filteredTrials.forEach((trial) => { const xValue = trial.params.find((p) => p.name === xAxis.name)?.param_internal_value || null @@ -218,8 +217,8 @@ const getAxisInfoForCategorical = ( return { name: param, range: [min, max], - is_log: false, - is_cat: true, + isLog: false, + isCat: true, } } @@ -232,14 +231,14 @@ const getAxisInfoForNumerical = ( (trial) => trial.params.find((p) => p.name === param)?.param_internal_value || null ) - const non_null_values: number[] = [] + const nonNullValues: number[] = [] values.forEach((value) => { if (value !== null) { - non_null_values.push(value) + nonNullValues.push(value) } }) - let min = Math.min(...non_null_values) - let max = Math.max(...non_null_values) + let min = Math.min(...nonNullValues) + let max = Math.max(...nonNullValues) if (distribution.log) { const padding = (Math.log10(max) - Math.log10(min)) * PADDING_RATIO min = Math.pow(10, Math.log10(min) - padding) @@ -253,58 +252,58 @@ const getAxisInfoForNumerical = ( return { name: param, range: [min, max], - is_log: distribution.log, - is_cat: false, + isLog: distribution.log, + isCat: false, } } const getColors = (values: number[]): number[] => { - const raw_ranks = getOrderWithSameOrderAveraging(values) - let color_idxs: number[] = [] + const rawRanks = getOrderWithSameOrderAveraging(values) + let colorIdxs: number[] = [] if (values.length > 2) { - color_idxs = raw_ranks.map((rank) => rank / (values.length - 1)) + colorIdxs = rawRanks.map((rank) => rank / (values.length - 1)) } else { - color_idxs = [0.5] + colorIdxs = [0.5] } - return color_idxs + return colorIdxs } const getOrderWithSameOrderAveraging = (values: number[]): number[] => { - const sorted_values = values.slice().sort() + const sortedValues = values.slice().sort() const ranks: number[] = [] values.forEach((value) => { - const first_index = sorted_values.indexOf(value) - const last_index = sorted_values.lastIndexOf(value) - const sum_of_the_value = - ((first_index + last_index) * (last_index - first_index + 1)) / 2 - const rank = sum_of_the_value / (last_index - first_index + 1) + const firstIndex = sortedValues.indexOf(value) + const lastIndex = sortedValues.lastIndexOf(value) + const sumOfTheValue = + ((firstIndex + lastIndex) * (lastIndex - firstIndex + 1)) / 2 + const rank = sumOfTheValue / (lastIndex - firstIndex + 1) ranks.push(rank) }) return ranks } -const plotRank = (rank_plot_info: RankPlotInfo | null, mode: string) => { +const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { if (document.getElementById(plotDomId) === null) { return } - if (rank_plot_info === null) { + if (rankPlotInfo === null) { plotly.react(plotDomId, [], { template: mode === "dark" ? plotlyDarkTemplate : {}, }) return } - const xAxis = rank_plot_info.xaxis - const yAxis = rank_plot_info.yaxis + const xAxis = rankPlotInfo.xaxis + const yAxis = rankPlotInfo.yaxis const layout: Partial = { xaxis: { title: xAxis.name, - type: xAxis.is_cat ? "category" : xAxis.is_log ? "log" : "linear", + type: xAxis.isCat ? "category" : xAxis.isLog ? "log" : "linear", }, yaxis: { title: yAxis.name, - type: yAxis.is_cat ? "category" : yAxis.is_log ? "log" : "linear", + type: yAxis.isCat ? "category" : yAxis.isLog ? "log" : "linear", }, margin: { l: 50, @@ -318,10 +317,10 @@ const plotRank = (rank_plot_info: RankPlotInfo | null, mode: string) => { const plotData: Partial[] = [ { type: "scatter", - x: rank_plot_info.xvalues, - y: rank_plot_info.yvalues, + x: rankPlotInfo.xvalues, + y: rankPlotInfo.yvalues, marker: { - color: rank_plot_info.colors, + color: rankPlotInfo.colors, colorscale: "Portland", colorbar: { title: "Rank", @@ -335,7 +334,7 @@ const plotRank = (rank_plot_info: RankPlotInfo | null, mode: string) => { mode: "markers", showlegend: false, hovertemplate: "%{hovertext}", - hovertext: rank_plot_info.hovertext, + hovertext: rankPlotInfo.hovertext, }, ] plotly.react(plotDomId, plotData, layout) From 29a5833cd02d5edb1c4699539688058b1bbb0f57 Mon Sep 17 00:00:00 2001 From: c-bata Date: Thu, 26 Oct 2023 15:40:19 +0900 Subject: [PATCH 049/176] Fix broken tutorial links --- docs/tutorials/preferential-optimization.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/preferential-optimization.rst b/docs/tutorials/preferential-optimization.rst index a192e883..ae662383 100644 --- a/docs/tutorials/preferential-optimization.rst +++ b/docs/tutorials/preferential-optimization.rst @@ -5,11 +5,11 @@ What is Preferential Optimization? ---------------------------------- Preferential optimization is a method for optimizing hyperparameters, focusing of human preferences, by determining which trial is superior when comparing a pair. -It differs from `human-in-the-loop optimization utilizing objective form widgets `_, +It differs from :ref:`human-in-the-loop optimization utilizing objective form widgets `, which relies on absolute evaluations, as it significantly reduces fluctuations in evaluators' criteria, thus ensuring more consistent results. In this tutorial, we'll interactively optimize RGB values to generate a color resembling a "sunset hue", -aligining with the problem setting in `this tutorial `_. +aligining with the problem setting in :ref:`this tutorial `. Familiarity with the tutorial ob objective form widgets may enhance your understanding. How to Run Preferential Optimization From 31360a4597863da09019edbbfbec6060e8c584c1 Mon Sep 17 00:00:00 2001 From: Victoria A <52001888+adjeiv@users.noreply.github.com> Date: Fri, 27 Oct 2023 08:21:49 +0100 Subject: [PATCH 050/176] Lint --- optuna_dashboard/ts/components/DataGrid.tsx | 4 +++- standalone_app/src/components/DataGrid.tsx | 4 +++- standalone_app/src/components/TrialTable.tsx | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/optuna_dashboard/ts/components/DataGrid.tsx b/optuna_dashboard/ts/components/DataGrid.tsx index d19ec9d9..24e45dc7 100644 --- a/optuna_dashboard/ts/components/DataGrid.tsx +++ b/optuna_dashboard/ts/components/DataGrid.tsx @@ -359,7 +359,9 @@ function stableSort( stabilizedThis.sort((a, b) => { if (less) { const ascending = order == "asc" - const result = ascending ? -less(a[0], b[0], ascending) : less(a[0], b[0], ascending) + const result = ascending + ? -less(a[0], b[0], ascending) + : less(a[0], b[0], ascending) if (result !== 0) return result } else { const result = comparator(a[0], b[0]) diff --git a/standalone_app/src/components/DataGrid.tsx b/standalone_app/src/components/DataGrid.tsx index d19ec9d9..24e45dc7 100644 --- a/standalone_app/src/components/DataGrid.tsx +++ b/standalone_app/src/components/DataGrid.tsx @@ -359,7 +359,9 @@ function stableSort( stabilizedThis.sort((a, b) => { if (less) { const ascending = order == "asc" - const result = ascending ? -less(a[0], b[0], ascending) : less(a[0], b[0], ascending) + const result = ascending + ? -less(a[0], b[0], ascending) + : less(a[0], b[0], ascending) if (result !== 0) return result } else { const result = comparator(a[0], b[0]) diff --git a/standalone_app/src/components/TrialTable.tsx b/standalone_app/src/components/TrialTable.tsx index 5a0efc96..97a04f3c 100644 --- a/standalone_app/src/components/TrialTable.tsx +++ b/standalone_app/src/components/TrialTable.tsx @@ -65,7 +65,7 @@ export const TrialTable: FC<{ return 0 } if (firstVal === undefined) { - return ascending ? -1 : 1 + return ascending ? -1 : 1 } else if (secondVal === undefined) { return ascending ? 1 : -1 } From f4405f033311c987e75febb24bbeb861128854a7 Mon Sep 17 00:00:00 2001 From: gen740 Date: Fri, 27 Oct 2023 17:30:33 +0900 Subject: [PATCH 051/176] Add bordor to the Study Artifact Card Content --- .../ts/components/StudyArtifactCards.tsx | 8 +------ .../ts/components/StudyHistory.tsx | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/optuna_dashboard/ts/components/StudyArtifactCards.tsx b/optuna_dashboard/ts/components/StudyArtifactCards.tsx index 71143ed4..c6394896 100644 --- a/optuna_dashboard/ts/components/StudyArtifactCards.tsx +++ b/optuna_dashboard/ts/components/StudyArtifactCards.tsx @@ -40,13 +40,6 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { return ( <> - - Study Artifacts Test - - {study.artifacts.map((artifact) => { const urlPath = `/artifacts/${study.id}/${artifact.artifact_id}` @@ -57,6 +50,7 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { marginBottom: theme.spacing(2), width: width, margin: theme.spacing(0, 1, 1, 0), + border: `1px solid ${theme.palette.divider}`, }} > = ({ studyId }) => { - {artifactEnabled && studyDetail !== null && ( - - )} + + + Study Artifacts + + {artifactEnabled && studyDetail !== null && ( + + )} + From d8bdea0a8c6b4c6de703b2764424509ee72807d4 Mon Sep 17 00:00:00 2001 From: gen740 Date: Fri, 27 Oct 2023 18:10:34 +0900 Subject: [PATCH 052/176] Delete delete function when artifact is not modifiable --- .../ts/components/TrialArtifactCards.tsx | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/optuna_dashboard/ts/components/TrialArtifactCards.tsx b/optuna_dashboard/ts/components/TrialArtifactCards.tsx index 3e15f7ed..d4d00e4c 100644 --- a/optuna_dashboard/ts/components/TrialArtifactCards.tsx +++ b/optuna_dashboard/ts/components/TrialArtifactCards.tsx @@ -34,6 +34,9 @@ export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => { useDeleteArtifactDialog() const [openThreejsArtifactModal, renderThreejsArtifactModal] = useThreejsArtifactModal() + const isArtifactModifiable = (trial: Trial) => { + return trial.state === "Running" || trial.state === "Waiting" + } const width = "200px" const height = "150px" @@ -75,11 +78,11 @@ export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => { p: theme.spacing(0.5, 0), flexGrow: 1, wordWrap: "break-word", - maxWidth: `calc(100% - ${ - isThreejsArtifact(artifact) - ? theme.spacing(12) - : theme.spacing(8) - })`, + maxWidth: `calc(100% - ${theme.spacing( + 4 + + (isThreejsArtifact(artifact) ? 4 : 0) + + (isArtifactModifiable(trial) ? 4 : 0) + )})`, }} > {artifact.filename} @@ -97,21 +100,23 @@ export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => { ) : null} - { - openDeleteArtifactDialog( - trial.study_id, - trial.trial_id, - artifact - ) - }} - > - - + {isArtifactModifiable(trial) ? ( + { + openDeleteArtifactDialog( + trial.study_id, + trial.trial_id, + artifact + ) + }} + > + + + ) : null} = ({ trial }) => { ) })} - + {isArtifactModifiable(trial) ? ( + + ) : null} {renderDeleteArtifactDialog()} {renderThreejsArtifactModal()} @@ -143,9 +150,6 @@ const TrialArtifactUploader: FC<{ const action = actionCreator() const [dragOver, setDragOver] = useState(false) - if (trial.state !== "Running" && trial.state !== "Waiting") { - return null - } const inputRef = useRef(null) const handleClick: MouseEventHandler = () => { if (!inputRef || !inputRef.current) { From 1c7645dfaf0afb13040f0bef0a73499d99833498 Mon Sep 17 00:00:00 2001 From: HideakiImamura Date: Tue, 31 Oct 2023 14:45:40 +0900 Subject: [PATCH 053/176] Fix for categorical variables --- optuna_dashboard/ts/components/GraphRank.tsx | 39 ++++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index 553612de..92cd8dff 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -159,10 +159,10 @@ const getRankPlotInfo = ( const hovertext: string[] = [] filteredTrials.forEach((trial) => { const xValue = - trial.params.find((p) => p.name === xAxis.name)?.param_internal_value || + trial.params.find((p) => p.name === xAxis.name)?.param_external_value || null const yValue = - trial.params.find((p) => p.name === yAxis.name)?.param_internal_value || + trial.params.find((p) => p.name === yAxis.name)?.param_external_value || null if (trial.values === undefined || xValue === null || yValue === null) { return @@ -314,11 +314,42 @@ const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { uirevision: "true", template: mode === "dark" ? plotlyDarkTemplate : {}, } + + let xValues = rankPlotInfo.xvalues + let yValues = rankPlotInfo.yvalues + if (xAxis.isCat && !yAxis.isCat) { + const xIndices: number[] = Array + .from(Array(xValues.length).keys()) + .sort((a, b) => xValues[a].toString().toLowerCase().localeCompare(xValues[b].toString().toLowerCase())) + xValues = xIndices.map((i) => xValues[i]) + yValues = xIndices.map((i) => yValues[i]) + } + if (!xAxis.isCat && yAxis.isCat) { + const yIndices: number[] = Array + .from(Array(yValues.length).keys()) + .sort((a, b) => yValues[a].toString().toLowerCase().localeCompare(yValues[b].toString().toLowerCase())) + xValues = yIndices.map((i) => xValues[i]) + yValues = yIndices.map((i) => yValues[i]) + } + if (xAxis.isCat && yAxis.isCat) { + const indices: number[] = Array + .from(Array(xValues.length).keys()) + .sort((a, b) => { + const xComp = xValues[a].toString().toLowerCase().localeCompare(xValues[b].toString().toLowerCase()) + if (xComp !== 0) { + return xComp + } + return yValues[a].toString().toLowerCase().localeCompare(yValues[b].toString().toLowerCase()) + }) + xValues = indices.map((i) => xValues[i]) + yValues = indices.map((i) => yValues[i]) + } + const plotData: Partial[] = [ { type: "scatter", - x: rankPlotInfo.xvalues, - y: rankPlotInfo.yvalues, + x: xValues, + y: yValues, marker: { color: rankPlotInfo.colors, colorscale: "Portland", From 8c02487afba39620fc616a70cebf6ea339638e66 Mon Sep 17 00:00:00 2001 From: HideakiImamura Date: Tue, 31 Oct 2023 16:32:07 +0900 Subject: [PATCH 054/176] Applt formatter --- optuna_dashboard/ts/components/GraphRank.tsx | 40 +++++++++++++------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index 92cd8dff..4e3e7544 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -314,33 +314,47 @@ const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { uirevision: "true", template: mode === "dark" ? plotlyDarkTemplate : {}, } - + let xValues = rankPlotInfo.xvalues let yValues = rankPlotInfo.yvalues if (xAxis.isCat && !yAxis.isCat) { - const xIndices: number[] = Array - .from(Array(xValues.length).keys()) - .sort((a, b) => xValues[a].toString().toLowerCase().localeCompare(xValues[b].toString().toLowerCase())) + const xIndices: number[] = Array.from(Array(xValues.length).keys()).sort( + (a, b) => + xValues[a] + .toString() + .toLowerCase() + .localeCompare(xValues[b].toString().toLowerCase()) + ) xValues = xIndices.map((i) => xValues[i]) yValues = xIndices.map((i) => yValues[i]) } if (!xAxis.isCat && yAxis.isCat) { - const yIndices: number[] = Array - .from(Array(yValues.length).keys()) - .sort((a, b) => yValues[a].toString().toLowerCase().localeCompare(yValues[b].toString().toLowerCase())) + const yIndices: number[] = Array.from(Array(yValues.length).keys()).sort( + (a, b) => + yValues[a] + .toString() + .toLowerCase() + .localeCompare(yValues[b].toString().toLowerCase()) + ) xValues = yIndices.map((i) => xValues[i]) yValues = yIndices.map((i) => yValues[i]) } if (xAxis.isCat && yAxis.isCat) { - const indices: number[] = Array - .from(Array(xValues.length).keys()) - .sort((a, b) => { - const xComp = xValues[a].toString().toLowerCase().localeCompare(xValues[b].toString().toLowerCase()) + const indices: number[] = Array.from(Array(xValues.length).keys()).sort( + (a, b) => { + const xComp = xValues[a] + .toString() + .toLowerCase() + .localeCompare(xValues[b].toString().toLowerCase()) if (xComp !== 0) { return xComp } - return yValues[a].toString().toLowerCase().localeCompare(yValues[b].toString().toLowerCase()) - }) + return yValues[a] + .toString() + .toLowerCase() + .localeCompare(yValues[b].toString().toLowerCase()) + } + ) xValues = indices.map((i) => xValues[i]) yValues = indices.map((i) => yValues[i]) } From 0c644462ce6b427f561b216106d70b7d9d6f178b Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Tue, 31 Oct 2023 08:35:55 +0100 Subject: [PATCH 055/176] [feat] Support intermediate value plots for constrained optimization --- .../ts/components/GraphIntermediateValues.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx index ca0bfda0..13412b87 100644 --- a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx +++ b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx @@ -84,16 +84,21 @@ const plotIntermediateValue = ( const values = trial.intermediate_values.filter( (iv) => iv.value !== "inf" && iv.value !== "-inf" && iv.value !== "nan" ) + const isFeasible = trial.constraints.every((c) => c <= 0) + let name = `trial #${trial.number}` + if (trial.state === "Running") { + name += ` (running)` + } else { + name += isFeasible ? `` : ` (infeasible)` + } return { x: values.map((iv) => iv.step), y: values.map((iv) => iv.value), marker: { maxdisplayed: 10 }, mode: "lines+markers", type: "scatter", - name: - trial.state !== "Running" - ? `trial #${trial.number}` - : `trial #${trial.number} (running)`, + name, + ...(isFeasible && { line: { color: "#CCCCCC" } }), } }) plotly.react(plotDomId, plotData, layout) From 26329560928fcad588c4eb337c2c6eaad430c157 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Tue, 31 Oct 2023 08:53:44 +0100 Subject: [PATCH 056/176] [feat] Make the infeasible plots dashed --- optuna_dashboard/ts/components/GraphIntermediateValues.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx index 13412b87..a4099c57 100644 --- a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx +++ b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx @@ -98,7 +98,7 @@ const plotIntermediateValue = ( mode: "lines+markers", type: "scatter", name, - ...(isFeasible && { line: { color: "#CCCCCC" } }), + ...(isFeasible && { line: { color: "#CCCCCC", dash: "dash" } }), } }) plotly.react(plotDomId, plotData, layout) From 0e1feb3cf6f51e0dd5360ff108bff643080d222f Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Tue, 31 Oct 2023 11:08:28 +0100 Subject: [PATCH 057/176] Fix an error in infeasible intermediate value plot --- optuna_dashboard/ts/components/GraphIntermediateValues.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx index a4099c57..9ee4a36d 100644 --- a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx +++ b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx @@ -98,7 +98,7 @@ const plotIntermediateValue = ( mode: "lines+markers", type: "scatter", name, - ...(isFeasible && { line: { color: "#CCCCCC", dash: "dash" } }), + ...(!isFeasible && { line: { color: "#CCCCCC", dash: "dash" } }), } }) plotly.react(plotDomId, plotData, layout) From 00606bc3f5118b6392c3b4140693928ed9f21cb5 Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Wed, 1 Nov 2023 18:27:54 +0900 Subject: [PATCH 058/176] Add constraints handling for contour plot. --- optuna_dashboard/ts/components/GraphContour.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphContour.tsx b/optuna_dashboard/ts/components/GraphContour.tsx index b186fffe..a5416f5b 100644 --- a/optuna_dashboard/ts/components/GraphContour.tsx +++ b/optuna_dashboard/ts/components/GraphContour.tsx @@ -195,12 +195,16 @@ const plotContour = ( const xValues: plotly.Datum[] = [] const yValues: plotly.Datum[] = [] const zValues: plotly.Datum[][] = new Array(yIndices.length) + const feasibleXY = new Set() for (let j = 0; j < yIndices.length; j++) { zValues[j] = new Array(xIndices.length).fill(null) } filteredTrials.forEach((trial, i) => { if (xAxis.values[i] && yAxis.values[i] && trial.values) { + if (trial.constraints.every((c) => c <= 0)) { + feasibleXY.add(xValues.length) + } const xValue = xAxis.values[i] as string | number const yValue = yAxis.values[i] as string | number xValues.push(xValue) @@ -234,12 +238,20 @@ const plotContour = ( }, { type: "scatter", - x: xValues, - y: yValues, + x: xValues.filter((_, i) => feasibleXY.has(i)), + y: yValues.filter((_, i) => feasibleXY.has(i)), marker: { line: { width: 2.0, color: "Grey" }, color: "black" }, mode: "markers", showlegend: false, }, + { + type: "scatter", + x: xValues.filter((_, i) => !feasibleXY.has(i)), + y: yValues.filter((_, i) => !feasibleXY.has(i)), + marker: { line: { width: 2.0, color: "Grey" }, color: "#cccccc" }, + mode: "markers", + showlegend: false, + }, ] plotly.react(plotDomId, plotData, layout) return From 79b50cc5005eb81b841cfafc6f483f7e4251f896 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Thu, 2 Nov 2023 04:56:25 +0100 Subject: [PATCH 059/176] Address the mamu's comment --- .../ts/components/GraphIntermediateValues.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx index 9ee4a36d..7ccce637 100644 --- a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx +++ b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx @@ -85,12 +85,13 @@ const plotIntermediateValue = ( (iv) => iv.value !== "inf" && iv.value !== "-inf" && iv.value !== "nan" ) const isFeasible = trial.constraints.every((c) => c <= 0) - let name = `trial #${trial.number}` - if (trial.state === "Running") { - name += ` (running)` - } else { - name += isFeasible ? `` : ` (infeasible)` - } + const name = `trial #${trial.number} ${ + trial.state === "Running" + ? "(running)" + : !isFeasible + ? " (infeasible)" + : "" + }` return { x: values.map((iv) => iv.step), y: values.map((iv) => iv.value), From 634f959d9039181a4a104a8d00245508e5d6a079 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Thu, 2 Nov 2023 05:00:39 +0100 Subject: [PATCH 060/176] Make it close to the original --- .../ts/components/GraphIntermediateValues.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx index 7ccce637..fc2be23e 100644 --- a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx +++ b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx @@ -85,20 +85,19 @@ const plotIntermediateValue = ( (iv) => iv.value !== "inf" && iv.value !== "-inf" && iv.value !== "nan" ) const isFeasible = trial.constraints.every((c) => c <= 0) - const name = `trial #${trial.number} ${ - trial.state === "Running" - ? "(running)" - : !isFeasible - ? " (infeasible)" - : "" - }` return { x: values.map((iv) => iv.step), y: values.map((iv) => iv.value), marker: { maxdisplayed: 10 }, mode: "lines+markers", type: "scatter", - name, + name: `trial #${trial.number} ${ + trial.state === "Running" + ? "(running)" + : !isFeasible + ? "(infeasible)" + : "" + }`, ...(!isFeasible && { line: { color: "#CCCCCC", dash: "dash" } }), } }) From 2403246749fa0e4e882c968e265ef7af5204d6a9 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Thu, 2 Nov 2023 06:33:34 +0100 Subject: [PATCH 061/176] Remove dashed lines for now --- optuna_dashboard/ts/components/GraphIntermediateValues.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx index fc2be23e..2d86e464 100644 --- a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx +++ b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx @@ -98,7 +98,7 @@ const plotIntermediateValue = ( ? "(infeasible)" : "" }`, - ...(!isFeasible && { line: { color: "#CCCCCC", dash: "dash" } }), + ...(!isFeasible && { line: { color: "#CCCCCC" } }), } }) plotly.react(plotDomId, plotData, layout) From 6bec7e3a80a6c07f452265adbdb9dcb6a0de6d6d Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Thu, 2 Nov 2023 16:24:50 +0900 Subject: [PATCH 062/176] Support constrained optimization for timeline plot --- .../ts/components/GraphTimeline.tsx | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphTimeline.tsx b/optuna_dashboard/ts/components/GraphTimeline.tsx index 68afe0fc..40d23662 100644 --- a/optuna_dashboard/ts/components/GraphTimeline.tsx +++ b/optuna_dashboard/ts/components/GraphTimeline.tsx @@ -92,12 +92,7 @@ const plotTimeline = (trials: Trial[], mode: string) => { template: mode === "dark" ? plotlyDarkTemplate : {}, } - const traces: Partial[] = [] - for (const s of Object.keys(cm) as TrialState[]) { - const bars = trials.filter((t) => t.state === s) - if (bars.length === 0) { - continue - } + const makeTrace = (bars: Trial[], name: string, color: string) => { const starts = bars.map((b) => b.datetime_start ?? new Date()) const completes = bars.map((b, i) => b.datetime_complete ?? starts[i]) const trace: Partial = { @@ -106,14 +101,38 @@ const plotTimeline = (trials: Trial[], mode: string) => { y: bars.map((b) => b.number), // @ts-ignore: To suppress ts(2322) base: starts.map((s) => s.toISOString()), - name: s, + name: name, text: bars.map((b) => makeHovertext(b)), - hovertemplate: "%{text}" + s + "", + hovertemplate: "%{text}" + name + "", orientation: "h", - marker: { color: cm[s] }, + marker: { color: color }, textposition: "none", // Avoid drawing hovertext in a bar. } - traces.push(trace) + return trace + } + + const traces: Partial[] = [] + for (const s of Object.keys(cm) as TrialState[]) { + const bars = trials.filter((t) => t.state === s) + if (bars.length === 0) { + continue + } + if (s == "Complete") { + const feasibleTrials = bars.filter((t) => + t.constraints.every((c) => c <= 0) + ) + const infeasibleTrials = bars.filter((t) => + t.constraints.some((c) => c > 0) + ) + if (feasibleTrials.length > 0) { + traces.push(makeTrace(feasibleTrials, "Complete", cm[s])) + } + if (infeasibleTrials.length > 0) { + traces.push(makeTrace(infeasibleTrials, "Infeasible", "#cccccc")) + } + } else { + traces.push(makeTrace(bars, s, cm[s])) + } } plotly.react(plotDomId, traces, layout) } From 7dbef5050115948aeb5508e84fcca10d9730a15a Mon Sep 17 00:00:00 2001 From: gen740 Date: Fri, 3 Nov 2023 04:02:35 +0900 Subject: [PATCH 063/176] Apply Format --- optuna_dashboard/ts/components/DeleteArtifactDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx b/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx index 4bf5e95d..7ac46162 100644 --- a/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx +++ b/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx @@ -11,7 +11,7 @@ import { actionCreator } from "../action" export const useDeleteTrialArtifactDialog = (): [ (studyId: number, trialId: number, artifact: Artifact) => void, - () => ReactNode, + () => ReactNode ] => { const action = actionCreator() @@ -58,7 +58,7 @@ export const useDeleteTrialArtifactDialog = (): [ export const useDeleteStudyArtifactDialog = (): [ (studyId: number, artifact: Artifact) => void, - () => ReactNode, + () => ReactNode ] => { const action = actionCreator() From 102c910bf76cdd8de7461ae7418b73f7190268c2 Mon Sep 17 00:00:00 2001 From: Kenshin Abe Date: Tue, 7 Nov 2023 01:31:00 +0900 Subject: [PATCH 064/176] Unify duplicated logic in plotSlice --- optuna_dashboard/ts/components/GraphSlice.tsx | 61 ++++++------------- 1 file changed, 20 insertions(+), 41 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphSlice.tsx b/optuna_dashboard/ts/components/GraphSlice.tsx index f37a5413..1498f451 100644 --- a/optuna_dashboard/ts/components/GraphSlice.tsx +++ b/optuna_dashboard/ts/components/GraphSlice.tsx @@ -196,57 +196,36 @@ const plotSlice = ( ) const trialNumbers: number[] = trials.map((t) => t.number) - if (selectedParamSpace.distribution.type !== "CategoricalDistribution") { - const trace: plotly.Data[] = [ - { - type: "scatter", - x: values, - y: objectiveValues, - mode: "markers", - marker: { - color: trialNumbers, - colorscale: "Blues", - reversescale: true, - colorbar: { - title: "Trial", - }, - line: { - color: "Grey", - width: 0.5, - }, + const trace: plotly.Data[] = [ + { + type: "scatter", + x: values, + y: objectiveValues, + mode: "markers", + marker: { + color: trialNumbers, + colorscale: "Blues", + reversescale: true, + colorbar: { + title: "Trial", + }, + line: { + color: "Grey", + width: 0.5, }, }, - ] + }, + ] + if (selectedParamSpace.distribution.type !== "CategoricalDistribution") { layout["xaxis"] = { title: selectedParamTarget.toLabel(), type: isLogScale(selectedParamSpace) ? "log" : "linear", gridwidth: 1, automargin: true, // Otherwise the label is outside of the plot } - plotly.react(plotDomId, trace, layout) } else { const vocabArr = selectedParamSpace.distribution.choices.map((c) => c.value) const tickvals: number[] = vocabArr.map((v, i) => i) - const trace: plotly.Data[] = [ - { - type: "scatter", - x: values, - y: objectiveValues, - mode: "markers", - marker: { - color: trialNumbers, - colorscale: "Blues", - reversescale: true, - colorbar: { - title: "Trial", - }, - line: { - color: "Grey", - width: 0.5, - }, - }, - }, - ] layout["xaxis"] = { title: selectedParamTarget.toLabel(), type: "linear", @@ -255,6 +234,6 @@ const plotSlice = ( ticktext: vocabArr, automargin: true, // Otherwise the label is outside of the plot } - plotly.react(plotDomId, trace, layout) } + plotly.react(plotDomId, trace, layout) } From 2c50b5afeaa45bc9fff256a2c553f74547520e31 Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Wed, 8 Nov 2023 11:45:14 +0900 Subject: [PATCH 065/176] Pin Python version to 3.11 --- .github/workflows/python-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 69923d04..60dd6ea0 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -58,7 +58,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: '3.x' + python-version: '3.11' architecture: x64 - name: Install dependencies run: | From e0bc52573228e5b7863239915498cb9ecb59bb8c Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Wed, 8 Nov 2023 15:29:13 +0900 Subject: [PATCH 066/176] Update optuna_dashboard/ts/components/GraphTimeline.tsx Co-authored-by: Hideaki Imamura <38826298+HideakiImamura@users.noreply.github.com> --- optuna_dashboard/ts/components/GraphTimeline.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphTimeline.tsx b/optuna_dashboard/ts/components/GraphTimeline.tsx index 40d23662..b2a29888 100644 --- a/optuna_dashboard/ts/components/GraphTimeline.tsx +++ b/optuna_dashboard/ts/components/GraphTimeline.tsx @@ -112,12 +112,12 @@ const plotTimeline = (trials: Trial[], mode: string) => { } const traces: Partial[] = [] - for (const s of Object.keys(cm) as TrialState[]) { - const bars = trials.filter((t) => t.state === s) + for (const [state, color] of Object.entries(cm)) { + const bars = trials.filter((t) => t.state === state) if (bars.length === 0) { continue } - if (s == "Complete") { + if (state == "Complete") { const feasibleTrials = bars.filter((t) => t.constraints.every((c) => c <= 0) ) @@ -125,14 +125,15 @@ const plotTimeline = (trials: Trial[], mode: string) => { t.constraints.some((c) => c > 0) ) if (feasibleTrials.length > 0) { - traces.push(makeTrace(feasibleTrials, "Complete", cm[s])) + traces.push(makeTrace(feasibleTrials, "Complete", color)) } if (infeasibleTrials.length > 0) { traces.push(makeTrace(infeasibleTrials, "Infeasible", "#cccccc")) } } else { - traces.push(makeTrace(bars, s, cm[s])) + traces.push(makeTrace(bars, state, color)) } } + } plotly.react(plotDomId, traces, layout) } From ff6ce9ddc29af8f5878f962828407a5f1b25bd67 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Wed, 8 Nov 2023 15:37:21 +0900 Subject: [PATCH 067/176] Remove excessive parenthesis --- optuna_dashboard/ts/components/GraphTimeline.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/optuna_dashboard/ts/components/GraphTimeline.tsx b/optuna_dashboard/ts/components/GraphTimeline.tsx index b2a29888..23c65d51 100644 --- a/optuna_dashboard/ts/components/GraphTimeline.tsx +++ b/optuna_dashboard/ts/components/GraphTimeline.tsx @@ -134,6 +134,5 @@ const plotTimeline = (trials: Trial[], mode: string) => { traces.push(makeTrace(bars, state, color)) } } - } plotly.react(plotDomId, traces, layout) } From cbbe56a2894db374f5f4c981470091996636c705 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Wed, 8 Nov 2023 15:37:58 +0900 Subject: [PATCH 068/176] Use strict equality operator --- optuna_dashboard/ts/components/GraphTimeline.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/ts/components/GraphTimeline.tsx b/optuna_dashboard/ts/components/GraphTimeline.tsx index 23c65d51..195eaeed 100644 --- a/optuna_dashboard/ts/components/GraphTimeline.tsx +++ b/optuna_dashboard/ts/components/GraphTimeline.tsx @@ -117,7 +117,7 @@ const plotTimeline = (trials: Trial[], mode: string) => { if (bars.length === 0) { continue } - if (state == "Complete") { + if (state === "Complete") { const feasibleTrials = bars.filter((t) => t.constraints.every((c) => c <= 0) ) From 3628c31c7e66426f201472a26ce4346c80832c0a Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Wed, 8 Nov 2023 15:39:25 +0900 Subject: [PATCH 069/176] Change argument name to state --- optuna_dashboard/ts/components/GraphTimeline.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphTimeline.tsx b/optuna_dashboard/ts/components/GraphTimeline.tsx index 195eaeed..cc0f7f57 100644 --- a/optuna_dashboard/ts/components/GraphTimeline.tsx +++ b/optuna_dashboard/ts/components/GraphTimeline.tsx @@ -92,7 +92,7 @@ const plotTimeline = (trials: Trial[], mode: string) => { template: mode === "dark" ? plotlyDarkTemplate : {}, } - const makeTrace = (bars: Trial[], name: string, color: string) => { + const makeTrace = (bars: Trial[], state: string, color: string) => { const starts = bars.map((b) => b.datetime_start ?? new Date()) const completes = bars.map((b, i) => b.datetime_complete ?? starts[i]) const trace: Partial = { @@ -101,9 +101,9 @@ const plotTimeline = (trials: Trial[], mode: string) => { y: bars.map((b) => b.number), // @ts-ignore: To suppress ts(2322) base: starts.map((s) => s.toISOString()), - name: name, + name: state, text: bars.map((b) => makeHovertext(b)), - hovertemplate: "%{text}" + name + "", + hovertemplate: "%{text}" + state + "", orientation: "h", marker: { color: color }, textposition: "none", // Avoid drawing hovertext in a bar. From 5076818e46160dca255bcbd2453a5f1dd28261c3 Mon Sep 17 00:00:00 2001 From: Kenshin Abe Date: Wed, 8 Nov 2023 18:42:41 +0900 Subject: [PATCH 070/176] Support constraints in slice plot --- optuna_dashboard/ts/components/GraphSlice.tsx | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphSlice.tsx b/optuna_dashboard/ts/components/GraphSlice.tsx index 1498f451..2a807cb7 100644 --- a/optuna_dashboard/ts/components/GraphSlice.tsx +++ b/optuna_dashboard/ts/components/GraphSlice.tsx @@ -188,22 +188,38 @@ const plotSlice = ( return } - const objectiveValues: number[] = trials.map( + const feasibleTrials: Trial[] = [] + const infeasibleTrials: Trial[] = [] + trials.forEach((t) => { + if (t.constraints.every((c) => c <= 0)) { + feasibleTrials.push(t) + } else { + infeasibleTrials.push(t) + } + }) + + const feasibleObjectiveValues: number[] = feasibleTrials.map( (t) => objectiveTarget.getTargetValue(t) as number ) - const values = trials.map( - (t) => selectedParamTarget.getTargetValue(t) as number + const infeasibleObjectiveValues: number[] = infeasibleTrials.map( + (t) => objectiveTarget.getTargetValue(t) as number ) - const trialNumbers: number[] = trials.map((t) => t.number) + const feasibleValues = feasibleTrials.map( + (t) => selectedParamTarget.getTargetValue(t) as number + ) + const infeasibleValues = infeasibleTrials.map( + (t) => selectedParamTarget.getTargetValue(t) as number + ) const trace: plotly.Data[] = [ { type: "scatter", - x: values, - y: objectiveValues, + x: feasibleValues, + y: feasibleObjectiveValues, mode: "markers", + name: "Feasible Trial", marker: { - color: trialNumbers, + color: feasibleTrials.map((t) => t.number), colorscale: "Blues", reversescale: true, colorbar: { @@ -215,6 +231,17 @@ const plotSlice = ( }, }, }, + { + type: "scatter", + x: infeasibleValues, + y: infeasibleObjectiveValues, + mode: "markers", + name: "Infeasible Trial", + marker: { + color: "#cccccc", + reversescale: true, + }, + }, ] if (selectedParamSpace.distribution.type !== "CategoricalDistribution") { layout["xaxis"] = { From a53d7a266c33de35e4c59ffac82ab15e55008b64 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 9 Nov 2023 13:49:33 +0900 Subject: [PATCH 071/176] Update GraphRank.tsx --- optuna_dashboard/ts/components/GraphRank.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index 4e3e7544..9763ac17 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -32,6 +32,7 @@ interface RankPlotInfo { yvalues: (string | number)[] zvalues: number[] colors: number[] + is_feasible: boolean[] hovertext: string[] } @@ -156,6 +157,7 @@ const getRankPlotInfo = ( const xValues: (string | number)[] = [] const yValues: (string | number)[] = [] const zValues: number[] = [] + const isFeasible: boolean[] = [] const hovertext: string[] = [] filteredTrials.forEach((trial) => { const xValue = @@ -168,9 +170,11 @@ const getRankPlotInfo = ( return } const zValue = Number(trial.values[objectiveId]) + const feasibility = trial.constraints.every((c) => c <= 0) xValues.push(xValue) yValues.push(yValue) zValues.push(zValue) + isFeasible.push(feasibility) hovertext.push(makeHovertext(trial)) }) @@ -183,6 +187,7 @@ const getRankPlotInfo = ( yvalues: yValues, zvalues: zValues, colors, + is_feasible: isFeasible, hovertext, } } @@ -382,5 +387,10 @@ const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { hovertext: rankPlotInfo.hovertext, }, ] + for (let i = 0; i < plotData.length; i++) { + if(rankPlotInfo.is_feasible[i] == false){ + plotData[i].marker.color = "#cccccc" + } + } plotly.react(plotDomId, plotData, layout) } From 00ce7ceb00ca2b581d33e12c6f358b2ab80f2528 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 9 Nov 2023 15:53:05 +0900 Subject: [PATCH 072/176] Update GraphRank.tsx --- optuna_dashboard/ts/components/GraphRank.tsx | 32 ++++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index 9763ac17..662135d2 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -367,10 +367,10 @@ const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { const plotData: Partial[] = [ { type: "scatter", - x: xValues, - y: yValues, + x: xValues.filter((_, i) => rankPlotInfo.is_feasible[i]), + y: yValues.filter((_, i) => rankPlotInfo.is_feasible[i]), marker: { - color: rankPlotInfo.colors, + color: rankPlotInfo.colors.filter((_, i) => rankPlotInfo.is_feasible[i]), colorscale: "Portland", colorbar: { title: "Rank", @@ -382,15 +382,29 @@ const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { }, }, mode: "markers", + name: "Feasible Trial", showlegend: false, hovertemplate: "%{hovertext}", - hovertext: rankPlotInfo.hovertext, + hovertext: rankPlotInfo.hovertext.filter((_, i) => rankPlotInfo.is_feasible[i]), }, - ] - for (let i = 0; i < plotData.length; i++) { - if(rankPlotInfo.is_feasible[i] == false){ - plotData[i].marker.color = "#cccccc" + { + type: "scatter", + x: xValues.filter((_, i) => !rankPlotInfo.is_feasible[i]), + y: yValues.filter((_, i) => !rankPlotInfo.is_feasible[i]), + marker: { + color: "#cccccc", + size: 10, + line: { + color: "Grey", + width: 0.5, + }, + }, + mode: "markers", + name: "Infeasible Trial", + showlegend: false, + hovertemplate: "%{hovertext}", + hovertext: rankPlotInfo.hovertext.filter((_, i) => !rankPlotInfo.is_feasible[i]), } - } + ] plotly.react(plotDomId, plotData, layout) } From 9182fba762c3dc367d07b5fec555ef4dda1a50d5 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 9 Nov 2023 17:13:46 +0900 Subject: [PATCH 073/176] fix lint --- optuna_dashboard/ts/components/GraphRank.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index 662135d2..6781793f 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -370,7 +370,9 @@ const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { x: xValues.filter((_, i) => rankPlotInfo.is_feasible[i]), y: yValues.filter((_, i) => rankPlotInfo.is_feasible[i]), marker: { - color: rankPlotInfo.colors.filter((_, i) => rankPlotInfo.is_feasible[i]), + color: rankPlotInfo.colors.filter( + (_, i) => rankPlotInfo.is_feasible[i] + ), colorscale: "Portland", colorbar: { title: "Rank", @@ -382,10 +384,11 @@ const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { }, }, mode: "markers", - name: "Feasible Trial", showlegend: false, hovertemplate: "%{hovertext}", - hovertext: rankPlotInfo.hovertext.filter((_, i) => rankPlotInfo.is_feasible[i]), + hovertext: rankPlotInfo.hovertext.filter( + (_, i) => rankPlotInfo.is_feasible[i] + ), }, { type: "scatter", @@ -400,11 +403,12 @@ const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { }, }, mode: "markers", - name: "Infeasible Trial", showlegend: false, hovertemplate: "%{hovertext}", - hovertext: rankPlotInfo.hovertext.filter((_, i) => !rankPlotInfo.is_feasible[i]), - } + hovertext: rankPlotInfo.hovertext.filter( + (_, i) => !rankPlotInfo.is_feasible[i] + ), + }, ] plotly.react(plotDomId, plotData, layout) } From fa3fa6241337caf1eaec918b331a2e45f6afb90d Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Wed, 8 Nov 2023 11:08:06 +0900 Subject: [PATCH 074/176] Replace example files with links. --- .gitignore | 1 - docs/tutorials/hitl.rst | 28 +++--- examples/hitl/main.py | 98 ------------------- .../preferential-optimization/evaluator.sh | 2 - .../preferential-optimization/generator.py | 60 ------------ examples/streamlit_plugin/rgb_evaluator.py | 54 ---------- examples/streamlit_plugin/rgb_generator.py | 81 --------------- 7 files changed, 14 insertions(+), 310 deletions(-) delete mode 100644 examples/hitl/main.py delete mode 100755 examples/preferential-optimization/evaluator.sh delete mode 100644 examples/preferential-optimization/generator.py delete mode 100644 examples/streamlit_plugin/rgb_evaluator.py delete mode 100644 examples/streamlit_plugin/rgb_generator.py diff --git a/.gitignore b/.gitignore index 49c137e0..646c1439 100644 --- a/.gitignore +++ b/.gitignore @@ -39,5 +39,4 @@ coverage.xml .vscode/ .DS_Store tmp/ -examples/preferential-optimization/artifact/ diff --git a/docs/tutorials/hitl.rst b/docs/tutorials/hitl.rst index e06709c8..ad4f5e7d 100644 --- a/docs/tutorials/hitl.rst +++ b/docs/tutorials/hitl.rst @@ -95,7 +95,7 @@ Given the above system, we carry out HITL optimization as follows: Environment setup ^^^^^^^^^^^^^^^^^ -To run `the script `_ used in this tutorial, you need to install following libraries: +To run `the script `_ used in this tutorial, you need to install following libraries: .. code-block:: console @@ -109,7 +109,7 @@ You will use SQLite for the storage backend in this tutorial. Ensure that the fo Execution of the HITL optimization script ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Run a python script below which you copied from `main.py `_ +Run a python script below which you copied from `main.py `_ .. code-block:: console @@ -150,7 +150,7 @@ Click the third item in the sidebar. You will see a list of all trials. .. image:: ./images/hitl10.png -For each trial, you can see its details such as RGB parameter values and importantly, the generated image based on these values. +For each trial, you can see its details such as RGB parameter values and importantly, the generated image based on these values. .. image:: ./images/hitl11.gif :width: 90% @@ -189,21 +189,21 @@ Let’s walk through the script we used for the optimization. r = trial.suggest_int("r", 0, 255) g = trial.suggest_int("g", 0, 255) b = trial.suggest_int("b", 0, 255) - + # 2. Generate image image_path = f"tmp/sample-{trial.number}.png" image = Image.new("RGB", (320, 240), color=(r, g, b)) image.save(image_path) - + # 3. Upload Artifact artifact_id = upload_artifact(trial, image_path, artifact_store) artifact_path = get_artifact_path(trial, artifact_id) - + # 4. Save Note note = textwrap.dedent( f"""\ ## Trial {trial.number} - + ![generated-image]({artifact_path}) """ ) @@ -222,10 +222,10 @@ In the ``suggest_and_generate_image`` function, a new Trial is obtained and new sampler=optuna.samplers.TPESampler(constant_liar=True, n_startup_trials=5), load_if_exists=True, ) - + # 2. Set an objective name study.set_metric_names(["Looks like sunset color?"]) - + # 3. Register ChoiceWidget register_objective_form_widgets( study, @@ -237,7 +237,7 @@ In the ``suggest_and_generate_image`` function, a new Trial is obtained and new ), ], ) - + # 4. Start Human-in-the-loop Optimization n_batch = 4 while True: @@ -259,17 +259,17 @@ The function ``start_optimization`` defines our loop for HITL optimization to ge def main() -> NoReturn: tmp_path = os.path.join(os.path.dirname(__file__), "tmp") - + # 1. Create Artifact Store artifact_path = os.path.join(os.path.dirname(__file__), "artifact") artifact_store = FileSystemArtifactStore(artifact_path) - + if not os.path.exists(artifact_path): os.mkdir(artifact_path) - + if not os.path.exists(tmp_path): os.mkdir(tmp_path) - + # 2. Run optimize loop start_optimization(artifact_store) diff --git a/examples/hitl/main.py b/examples/hitl/main.py deleted file mode 100644 index 7844bc7a..00000000 --- a/examples/hitl/main.py +++ /dev/null @@ -1,98 +0,0 @@ -import os -import textwrap -import time -from typing import NoReturn - -import optuna -from optuna.artifacts import FileSystemArtifactStore -from optuna.artifacts import upload_artifact -from optuna.trial import TrialState -from optuna_dashboard import ChoiceWidget -from optuna_dashboard import register_objective_form_widgets -from optuna_dashboard import save_note -from optuna_dashboard.artifact import get_artifact_path -from PIL import Image - - -def suggest_and_generate_image( - study: optuna.Study, artifact_store: FileSystemArtifactStore -) -> None: - # 1. Ask new parameters - trial = study.ask() - r = trial.suggest_int("r", 0, 255) - g = trial.suggest_int("g", 0, 255) - b = trial.suggest_int("b", 0, 255) - - # 2. Generate image - image_path = f"tmp/sample-{trial.number}.png" - image = Image.new("RGB", (320, 240), color=(r, g, b)) - image.save(image_path) - - # 3. Upload Artifact - artifact_id = upload_artifact(trial, image_path, artifact_store) - artifact_path = get_artifact_path(trial, artifact_id) - - # 4. Save Note - note = textwrap.dedent( - f"""\ - ## Trial {trial.number} - - ![generated-image]({artifact_path}) - """ - ) - save_note(trial, note) - - -def start_optimization(artifact_store: FileSystemArtifactStore) -> NoReturn: - # 1. Create Study - study = optuna.create_study( - study_name="Human-in-the-loop Optimization", - storage="sqlite:///db.sqlite3", - sampler=optuna.samplers.TPESampler(constant_liar=True, n_startup_trials=5), - load_if_exists=True, - ) - - # 2. Set an objective name - study.set_metric_names(["Looks like sunset color?"]) - - # 3. Register ChoiceWidget - register_objective_form_widgets( - study, - widgets=[ - ChoiceWidget( - choices=["Good 👍", "So-so👌", "Bad 👎"], - values=[-1, 0, 1], - description="Please input your score!", - ), - ], - ) - - # 4. Start Human-in-the-loop Optimization - n_batch = 4 - while True: - running_trials = study.get_trials(deepcopy=False, states=(TrialState.RUNNING,)) - if len(running_trials) >= n_batch: - time.sleep(1) # Avoid busy-loop - continue - suggest_and_generate_image(study, artifact_store) - - -def main() -> NoReturn: - tmp_path = os.path.join(os.path.dirname(__file__), "tmp") - - # 1. Create Artifact Store - artifact_path = os.path.join(os.path.dirname(__file__), "artifact") - artifact_store = FileSystemArtifactStore(artifact_path) - - if not os.path.exists(artifact_path): - os.mkdir(artifact_path) - - if not os.path.exists(tmp_path): - os.mkdir(tmp_path) - - # 2. Run optimize loop - start_optimization(artifact_store) - - -if __name__ == "__main__": - main() diff --git a/examples/preferential-optimization/evaluator.sh b/examples/preferential-optimization/evaluator.sh deleted file mode 100755 index f7ebeca5..00000000 --- a/examples/preferential-optimization/evaluator.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -optuna-dashboard sqlite:///example.db --artifact-dir ./artifact \ No newline at end of file diff --git a/examples/preferential-optimization/generator.py b/examples/preferential-optimization/generator.py deleted file mode 100644 index a26343bb..00000000 --- a/examples/preferential-optimization/generator.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -import os -import tempfile -import time -from typing import NoReturn - -from optuna.artifacts import FileSystemArtifactStore -from optuna.artifacts import upload_artifact -from optuna_dashboard import register_preference_feedback_component -from optuna_dashboard.preferential import create_study -from optuna_dashboard.preferential.samplers.gp import PreferentialGPSampler -from PIL import Image - - -STORAGE_URL = "sqlite:///example.db" -artifact_path = os.path.join(os.path.dirname(__file__), "artifact") -artifact_store = FileSystemArtifactStore(base_path=artifact_path) -os.makedirs(artifact_path, exist_ok=True) - - -def main() -> NoReturn: - study = create_study( - n_generate=4, - study_name="Preferential Optimization", - storage=STORAGE_URL, - sampler=PreferentialGPSampler(), - load_if_exists=True, - ) - # Change the component, displayed on the human feedback pages. - # By default (component_type="note"), the Trial's Markdown note is displayed. - user_attr_key = "rgb_image" - register_preference_feedback_component(study, "artifact", user_attr_key) - - with tempfile.TemporaryDirectory() as tmpdir: - while True: - # If study.should_generate() returns False, - # the generator waits for human evaluation. - if not study.should_generate(): - time.sleep(0.1) # Avoid busy-loop - continue - - trial = study.ask() - # 1. Ask new parameters - r = trial.suggest_int("r", 0, 255) - g = trial.suggest_int("g", 0, 255) - b = trial.suggest_int("b", 0, 255) - - # 2. Generate image - image_path = os.path.join(tmpdir, f"sample-{trial.number}.png") - image = Image.new("RGB", (320, 240), color=(r, g, b)) - image.save(image_path) - - # 3. Upload Artifact and set artifact_id to trial.user_attrs["rgb_image"]. - artifact_id = upload_artifact(trial, image_path, artifact_store) - trial.set_user_attr(user_attr_key, artifact_id) - - -if __name__ == "__main__": - main() diff --git a/examples/streamlit_plugin/rgb_evaluator.py b/examples/streamlit_plugin/rgb_evaluator.py deleted file mode 100644 index 5836dbd6..00000000 --- a/examples/streamlit_plugin/rgb_evaluator.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import annotations - -import os -import shutil -import tempfile -import uuid - -import optuna -from optuna.trial import TrialState -from optuna_dashboard.artifact.file_system import FileSystemBackend -from optuna_dashboard.streamlit import render_objective_form_widgets -from optuna_dashboard.streamlit import render_trial_note - -import streamlit as st - - -artifact_path = os.path.join(os.path.dirname(__file__), "artifact") -artifact_backend = FileSystemBackend(base_path=artifact_path) - - -def get_tmp_dir() -> str: - if "tmp_dir" not in st.session_state: - tmp_dir_name = str(uuid.uuid4()) - tmp_dir_path = os.path.join(tempfile.gettempdir(), tmp_dir_name) - os.makedirs(tmp_dir_path, exist_ok=True) - st.session_state.tmp_dir = tmp_dir_path - - return st.session_state.tmp_dir - - -def start_streamlit() -> None: - tmpdir = get_tmp_dir() - study = optuna.load_study( - storage="sqlite:///streamlit-db.sqlite3", study_name="Human-in-the-loop Optimization" - ) - selected_trial = st.sidebar.selectbox("Trial", study.trials, format_func=lambda t: t.number) - - if selected_trial is None: - return - render_trial_note(study, selected_trial) - artifact_id = selected_trial.user_attrs.get("artifact_id") - if artifact_id: - with artifact_backend.open(artifact_id) as fsrc: - tmp_img_path = os.path.join(tmpdir, artifact_id + ".png") - with open(tmp_img_path, "wb") as fdst: - shutil.copyfileobj(fsrc, fdst) - st.image(tmp_img_path, caption="Image") - - if selected_trial.state == TrialState.RUNNING: - render_objective_form_widgets(study, selected_trial) - - -if __name__ == "__main__": - start_streamlit() diff --git a/examples/streamlit_plugin/rgb_generator.py b/examples/streamlit_plugin/rgb_generator.py deleted file mode 100644 index fc76ad02..00000000 --- a/examples/streamlit_plugin/rgb_generator.py +++ /dev/null @@ -1,81 +0,0 @@ -from __future__ import annotations - -import os -import tempfile -import time -from typing import NoReturn - -import optuna -from optuna.trial import TrialState -from optuna_dashboard import ChoiceWidget -from optuna_dashboard import register_objective_form_widgets -from optuna_dashboard import save_note -from optuna_dashboard.artifact import upload_artifact -from optuna_dashboard.artifact.file_system import FileSystemBackend -from PIL import Image - - -def suggest_and_generate_image( - study: optuna.Study, artifact_backend: FileSystemBackend, tmpdir: str -) -> None: - # 1. Ask new parameters - trial = study.ask() - r = trial.suggest_int("r", 0, 255) - g = trial.suggest_int("g", 0, 255) - b = trial.suggest_int("b", 0, 255) - - # 2. Generate image - image_path = os.path.join(tmpdir, f"sample-{trial.number}.png") - image = Image.new("RGB", (320, 240), color=(r, g, b)) - image.save(image_path) - - # 3. Upload Artifact - artifact_id = upload_artifact(artifact_backend, trial, image_path) - trial.set_user_attr("artifact_id", artifact_id) - - # 4. Save Note - save_note(trial, f"## Trial {trial.number}") - - -def main() -> NoReturn: - # 1. Create Artifact Store - artifact_path = os.path.join(os.path.dirname(__file__), "artifact") - artifact_backend = FileSystemBackend(base_path=artifact_path) - - if not os.path.exists(artifact_path): - os.mkdir(artifact_path) - - # 2. Create Study - study = optuna.create_study( - study_name="Human-in-the-loop Optimization", - storage="sqlite:///streamlit-db.sqlite3", - sampler=optuna.samplers.TPESampler(constant_liar=True, n_startup_trials=5), - load_if_exists=True, - ) - study.set_metric_names(["Looks like sunset color?"]) - - # 4. Register ChoiceWidget - register_objective_form_widgets( - study, - widgets=[ - ChoiceWidget( - choices=["Good 👍", "So-so👌", "Bad 👎"], - values=[-1, 0, 1], - description="Please input your score!", - ), - ], - ) - - # 5. Start Human-in-the-loop Optimization - n_batch = 4 - with tempfile.TemporaryDirectory() as tmpdir: - while True: - running_trials = study.get_trials(deepcopy=False, states=(TrialState.RUNNING,)) - if len(running_trials) >= n_batch: - time.sleep(1) # Avoid busy-loop - continue - suggest_and_generate_image(study, artifact_backend, tmpdir) - - -if __name__ == "__main__": - main() From 985b301f67b54ebff12c400777cf76100c68a63c Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Fri, 10 Nov 2023 13:47:35 +0900 Subject: [PATCH 075/176] Add README to direct users to optuna-examples. --- examples/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 examples/README.md diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..ab86ed0a --- /dev/null +++ b/examples/README.md @@ -0,0 +1,5 @@ +Optuna Dashboard Examples +========================= + +Example files have been moved to the [optuna/optuna-examples](https://github.com/optuna/optuna-examples/) repoistory. +You can find the dashboard-related examples in the [dashboard](https://github.com/optuna/optuna-examples/tree/main/dashboard) directory. From bbe7fb2b227276ed17ef4e0b193c9b81f3b1664d Mon Sep 17 00:00:00 2001 From: HideakiImamura Date: Fri, 10 Nov 2023 14:35:22 +0900 Subject: [PATCH 076/176] Add E2E tests for preferential optimization --- CONTRIBUTING.md | 4 + .../test_preferential_optimization.py | 139 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04002ad7..df519b53 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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) diff --git a/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py b/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py new file mode 100644 index 00000000..e5ec3881 --- /dev/null +++ b/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py @@ -0,0 +1,139 @@ +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 suggest_and_save_user_attr(study: optuna.Study) -> None: + trial = study.ask() + r = trial.suggest_int("r", 0, 255) + g = trial.suggest_int("g", 0, 255) + b = trial.suggest_int("b", 0, 255) + + trial.set_user_attr("r+g+b", r + g + b) + + +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, + ) + + study.set_metric_names(["Looks like sunset color?"]) + + register_objective_form_widgets( + study, + widgets=[ + ChoiceWidget( + choices=["Good", "So-so", "Bad"], + values=[-1, 0, 1], + description="Please input your score!", + ), + ], + ) + + n_batch = 4 + while True: + running_trials = study.get_trials(deepcopy=False, states=(TrialState.RUNNING,)) + if len(running_trials) >= n_batch: + break + suggest_and_save_user_attr(study) + + 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() From da70fbe2fa0df813f84c2fc03a85a6e3a3f88063 Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Fri, 10 Nov 2023 15:02:48 +0900 Subject: [PATCH 077/176] Update URL. --- docs/tutorials/preferential-optimization.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/preferential-optimization.rst b/docs/tutorials/preferential-optimization.rst index ae662383..ab7ec654 100644 --- a/docs/tutorials/preferential-optimization.rst +++ b/docs/tutorials/preferential-optimization.rst @@ -99,7 +99,7 @@ enabling the Optuna Dashboard to display images on the evaluation feedback page. register_preference_feedback_component(study, "artifact", user_attr_key) Following this, we create a loop that continuously checks if new trials should be generated, awaiting human evaluation if not. -Within the while loop, new trials are generated if the condition :meth:`~optuna_dashboard.preferential.PreferentialStudy.should_generate` returns ``True``. +Within the while loop, new trials are generated if the condition :meth:`~optuna_dashboard.preferential.PreferentialStudy.should_generate` returns ``True``. For each trial, RGB values are sampled, an image is generated with these values, saved temporarily. Then the image is uploaded to the artifact store, and finally, the ``artifact_id`` is stored to the key, which is specified via :func:`~optuna_dashboard.register_preference_feedback_component`. @@ -127,4 +127,4 @@ Then the image is uploaded to the artifact store, and finally, the ``artifact_id artifact_id = upload_artifact(trial, image_path, artifact_store) trial.set_user_attr(user_attr_key, artifact_id) -.. _generator.py: https://github.com/optuna/optuna-dashboard/blob/main/examples/preferential-optimization/generator.py +.. _generator.py: https://github.com/optuna/optuna-examples/blob/main/dashboard/preferential-optimization/generator.py From fe376008af8000ecc40dab1e5c1a9368c8c9a4b6 Mon Sep 17 00:00:00 2001 From: HideakiImamura Date: Fri, 10 Nov 2023 15:09:51 +0900 Subject: [PATCH 078/176] Refactor test --- .../test_preferential_optimization.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py b/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py index e5ec3881..fa3317dc 100644 --- a/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py +++ b/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py @@ -11,15 +11,6 @@ import pytest from ...test_server import make_test_server -def suggest_and_save_user_attr(study: optuna.Study) -> None: - trial = study.ask() - r = trial.suggest_int("r", 0, 255) - g = trial.suggest_int("g", 0, 255) - b = trial.suggest_int("b", 0, 255) - - trial.set_user_attr("r+g+b", r + g + b) - - def make_test_storage() -> optuna.storages.InMemoryStorage: storage = optuna.storages.InMemoryStorage() sampler = optuna.samplers.RandomSampler(seed=0) @@ -30,15 +21,12 @@ def make_test_storage() -> optuna.storages.InMemoryStorage: sampler=sampler, ) - study.set_metric_names(["Looks like sunset color?"]) - register_objective_form_widgets( study, widgets=[ ChoiceWidget( choices=["Good", "So-so", "Bad"], values=[-1, 0, 1], - description="Please input your score!", ), ], ) @@ -48,7 +36,7 @@ def make_test_storage() -> optuna.storages.InMemoryStorage: running_trials = study.get_trials(deepcopy=False, states=(TrialState.RUNNING,)) if len(running_trials) >= n_batch: break - suggest_and_save_user_attr(study) + study.ask() return storage From 462f609f62831e3abdaf9fd3ac7fb9bfe0a4c2eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Nov 2023 09:01:48 +0000 Subject: [PATCH 079/176] Bump axios from 1.2.1 to 1.6.0 Bumps [axios](https://github.com/axios/axios) from 1.2.1 to 1.6.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.2.1...v1.6.0) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0561a75a..ec8e6d17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@react-three/drei": "^9.80.0", "@react-three/fiber": "^8.13.6", "@types/three": "^0.154.0", - "axios": "^1.2.1", + "axios": "^1.6.0", "elkjs": "^0.8.2", "notistack": "^3.0.1", "plotly.js-dist-min": "^2.22.0", @@ -5094,9 +5094,9 @@ } }, "node_modules/axios": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", - "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -18995,9 +18995,9 @@ "dev": true }, "axios": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", - "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", diff --git a/package.json b/package.json index a675a1ce..a60be438 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@react-three/drei": "^9.80.0", "@react-three/fiber": "^8.13.6", "@types/three": "^0.154.0", - "axios": "^1.2.1", + "axios": "^1.6.0", "elkjs": "^0.8.2", "notistack": "^3.0.1", "plotly.js-dist-min": "^2.22.0", From 2965e69508d4747d0b5bf9a43575fd498acbab1d Mon Sep 17 00:00:00 2001 From: Victoria A <52001888+adjeiv@users.noreply.github.com> Date: Sun, 12 Nov 2023 10:49:51 +0000 Subject: [PATCH 080/176] Retain trial notes --- optuna_dashboard/_app.py | 4 +++- optuna_dashboard/_note.py | 40 +++++++++++++++++++++++++++++++++----- python_tests/test_note.py | 41 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index 812201ad..9ce820b7 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -164,7 +164,8 @@ def create_app( if new_study_summary is None: response.status = 500 return {"reason": "Failed to load the new study"} - + + note.transfer_notes(storage, src_study, dst_study) storage.delete_study(src_study._study_id) response.status = 201 return serialize_study_summary(new_study_summary) @@ -176,6 +177,7 @@ def create_app( delete_all_artifacts(artifact_store, storage, study_id) try: + note.delete_study_notes(storage, study_id) storage.delete_study(study_id) except KeyError: response.status = 404 # Not found diff --git a/optuna_dashboard/_note.py b/optuna_dashboard/_note.py index 95853a3d..e510ddfd 100644 --- a/optuna_dashboard/_note.py +++ b/optuna_dashboard/_note.py @@ -109,6 +109,21 @@ def note_str_key_prefix(trial_id: Optional[int]) -> str: return prefix return f"dashboard:{trial_id}:note_str:" +def transfer_notes(storage: BaseStorage, src_study: optuna.Study, dst_study: optuna.Study) -> None: + system_attrs = storage.get_study_system_attrs(study_id=src_study._study_id) + + def transfer(src_trial_id: Optional[int], dst_trial_id: Optional[int]) -> None: + note = get_note_from_system_attrs(system_attrs, src_trial_id)["body"] + save_note_with_version(storage, dst_study._study_id, dst_trial_id, 0, note) + delete_notes(storage, src_study._study_id, src_trial_id) + + # Transfer individual trial notes + for src_trial, dst_trial in zip(src_study.get_trials(), dst_study.get_trials()): + transfer(src_trial._trial_id, dst_trial._trial_id) + + # Transfer study note + NO_SRC_TRIAL, NO_DST_TRIAL = None, None + transfer(NO_SRC_TRIAL, NO_DST_TRIAL) def get_note_from_system_attrs(system_attrs: dict[str, Any], trial_id: Optional[int]) -> NoteType: if note_ver_key(trial_id) not in system_attrs: @@ -131,6 +146,13 @@ def version_is_incremented( db_note_ver = system_attrs.get(note_ver_key(trial_id), 0) return req_note_ver == db_note_ver + 1 +def all_trial_notes(storage: BaseStorage, study_id: int, trial_id: Optional[int]) -> dict[str, str]: + all_note_attrs: dict[str, str] = { + key: value + for key, value in storage.get_study_system_attrs(study_id).items() + if key.startswith(note_str_key_prefix(trial_id)) + } + return all_note_attrs def save_note_with_version( storage: BaseStorage, study_id: int, trial_id: Optional[int], ver: int, body: str @@ -142,15 +164,23 @@ def save_note_with_version( storage.set_study_system_attr(study_id, k, v) # Clear previous messages - all_note_attrs: dict[str, str] = { - key: value - for key, value in storage.get_study_system_attrs(study_id).items() - if key.startswith(note_str_key_prefix(trial_id)) - } + all_note_attrs = all_trial_notes(storage, study_id, trial_id) if len(all_note_attrs) > len(attrs): for i in range(len(attrs), len(all_note_attrs)): storage.set_study_system_attr(study_id, f"{note_str_key_prefix(trial_id)}{i}", "") +def delete_study_notes(storage: BaseStorage, study_id): + study = storage.get_study_name_from_id(study_id) + for trial in storage.get_all_trials(study_id): + delete_notes(storage, study_id, trial._trial_id) + + delete_notes(storage, study_id, None) + +def delete_notes(storage: BaseStorage, study_id: int, trial_id: Optional[int]) -> None: + all_note_attrs = all_trial_notes(storage, study_id, trial_id) + + for i in range(len(all_note_attrs)): + storage.set_study_system_attr(study_id, f"{note_str_key_prefix(trial_id)}{i}", "") def split_body(note_str: str, trial_id: Optional[int]) -> dict[str, str]: note_len = len(note_str) diff --git a/python_tests/test_note.py b/python_tests/test_note.py index 7cc02cd4..4997d16a 100644 --- a/python_tests/test_note.py +++ b/python_tests/test_note.py @@ -53,3 +53,44 @@ class NoteTestCase(TestCase): note_dict = note.get_note_from_system_attrs(system_attrs, trial._trial_id) self.assertEqual(note_dict["body"], body) self.assertEqual(note_dict["version"], expected_ver) + + def test_delete_notes_trial(self) -> None: + study = optuna.create_study() + trial_1 = study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) + trial_2 = study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) + storage = study._storage + + for trial, body in [(trial_1, "version 1"), (trial_2, "version 2")]: + save_note(trial, body) + + # first assert existence + actual = get_note(trial) + self.assertEqual(actual, body) + + # delete + note.delete_notes(storage, study._study_id, trial._trial_id) + + # assert deletion + actual = get_note(trial) + self.assertEqual(actual, "") + + + note.delete_study_notes(storage, study._study_id) + + def test_delete_notes_study(self) -> None: + pass + + def test_transfer_notes(self) -> None: + study = optuna.create_study() + trial_1 = study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) + trial_2 = study.ask({"x2": optuna.distributions.FloatDistribution(0, 10)}) + storage = study._storage + + save_note(trial_1, "trial 1") + save_note(trial_2, "trial 2") + + new_study = optuna.create_study( + storage=storage, directions=study.directions + ) + note.transfer_notes(storage, study, new_study) + From f04c2d7d7bae0cd667eec056bc0a35b19f30b846 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Sun, 5 Nov 2023 16:32:18 +0900 Subject: [PATCH 081/176] Move getAxis implementation to graphUtil --- .../ts/components/GraphContour.tsx | 109 +---------------- optuna_dashboard/ts/graphUtil.ts | 112 ++++++++++++++++++ 2 files changed, 113 insertions(+), 108 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphContour.tsx b/optuna_dashboard/ts/components/GraphContour.tsx index a5416f5b..b7895ddf 100644 --- a/optuna_dashboard/ts/components/GraphContour.tsx +++ b/optuna_dashboard/ts/components/GraphContour.tsx @@ -14,25 +14,8 @@ import { import blue from "@mui/material/colors/blue" import { plotlyDarkTemplate } from "./PlotlyDarkMode" import { useMergedUnionSearchSpace } from "../searchSpace" +import { getAxisInfo } from "../graphUtil" -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const unique = (array: any[]) => { - const knownElements = new Map() - array.forEach((elem) => knownElements.set(elem, true)) - return Array.from(knownElements.keys()) -} - -type AxisInfo = { - name: string - min: number - max: number - isLog: boolean - isCat: boolean - indices: (string | number)[] - values: (string | number | null)[] -} - -const PADDING_RATIO = 0.05 const plotDomId = "graph-contour" export const Contour: FC<{ @@ -284,93 +267,3 @@ const plotContour = ( ] plotly.react(plotDomId, plotData, layout) } - -const getAxisInfoForNumericalParams = ( - trials: Trial[], - paramName: string, - distribution: FloatDistribution | IntDistribution -): AxisInfo => { - let min = 0 - let max = 0 - if (distribution.log) { - const padding = - (Math.log10(distribution.high) - Math.log10(distribution.low)) * - PADDING_RATIO - min = Math.pow(10, Math.log10(distribution.low) - padding) - max = Math.pow(10, Math.log10(distribution.high) + padding) - } else { - const padding = (distribution.high - distribution.low) * PADDING_RATIO - min = distribution.low - padding - max = distribution.high + padding - } - - const values = trials.map( - (trial) => - trial.params.find((p) => p.name === paramName)?.param_internal_value || - null - ) - const indices = unique(values) - .filter((v) => v !== null) - .sort((a, b) => a - b) - if (indices.length >= 2) { - indices.unshift(min) - indices.push(max) - } - return { - name: paramName, - min, - max, - isLog: distribution.log, - isCat: false, - indices, - values, - } -} - -const getAxisInfoForCategoricalParams = ( - trials: Trial[], - paramName: string, - distribution: CategoricalDistribution -): AxisInfo => { - const values = trials.map( - (trial) => - trial.params.find((p) => p.name === paramName)?.param_external_value || - null - ) - const isDynamic = values.some((v) => v === null) - const span = distribution.choices.length - (isDynamic ? 2 : 1) - const padding = span * PADDING_RATIO - const min = -padding - const max = span + padding - - const indices = distribution.choices - .map((c) => c.value) - .sort((a, b) => - a.toLowerCase() < b.toLowerCase() - ? -1 - : a.toLowerCase() > b.toLowerCase() - ? 1 - : 0 - ) - return { - name: paramName, - min, - max, - isLog: false, - isCat: true, - indices, - values, - } -} - -const getAxisInfo = (trials: Trial[], param: SearchSpaceItem): AxisInfo => { - if (param.distribution.type === "CategoricalDistribution") { - return getAxisInfoForCategoricalParams( - trials, - param.name, - param.distribution - ) - } else { - return getAxisInfoForNumericalParams(trials, param.name, param.distribution) - } -} diff --git a/optuna_dashboard/ts/graphUtil.ts b/optuna_dashboard/ts/graphUtil.ts index 1b1c69cb..4e6f74e8 100644 --- a/optuna_dashboard/ts/graphUtil.ts +++ b/optuna_dashboard/ts/graphUtil.ts @@ -1,3 +1,115 @@ +const PADDING_RATIO = 0.05 + +type AxisInfo = { + name: string + min: number + max: number + isLog: boolean + isCat: boolean + indices: (string | number)[] + values: (string | number | null)[] +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const unique = (array: any[]) => { + const knownElements = new Map() + array.forEach((elem) => knownElements.set(elem, true)) + return Array.from(knownElements.keys()) +} + +export const getAxisInfo = ( + trials: Trial[], + param: SearchSpaceItem +): AxisInfo => { + if (param.distribution.type === "CategoricalDistribution") { + return getAxisInfoForCategoricalParams( + trials, + param.name, + param.distribution + ) + } else { + return getAxisInfoForNumericalParams(trials, param.name, param.distribution) + } +} + +const getAxisInfoForCategoricalParams = ( + trials: Trial[], + paramName: string, + distribution: CategoricalDistribution +): AxisInfo => { + const values = trials.map( + (trial) => + trial.params.find((p) => p.name === paramName)?.param_external_value || + null + ) + const isDynamic = values.some((v) => v === null) + const span = distribution.choices.length - (isDynamic ? 2 : 1) + const padding = span * PADDING_RATIO + const min = -padding + const max = span + padding + + const indices = distribution.choices + .map((c) => c.value) + .sort((a, b) => + a.toLowerCase() < b.toLowerCase() + ? -1 + : a.toLowerCase() > b.toLowerCase() + ? 1 + : 0 + ) + return { + name: paramName, + min, + max, + isLog: false, + isCat: true, + indices, + values, + } +} + +const getAxisInfoForNumericalParams = ( + trials: Trial[], + paramName: string, + distribution: FloatDistribution | IntDistribution +): AxisInfo => { + let min = 0 + let max = 0 + if (distribution.log) { + const padding = + (Math.log10(distribution.high) - Math.log10(distribution.low)) * + PADDING_RATIO + min = Math.pow(10, Math.log10(distribution.low) - padding) + max = Math.pow(10, Math.log10(distribution.high) + padding) + } else { + const padding = (distribution.high - distribution.low) * PADDING_RATIO + min = distribution.low - padding + max = distribution.high + padding + } + + const values = trials.map( + (trial) => + trial.params.find((p) => p.name === paramName)?.param_internal_value || + null + ) + const indices = unique(values) + .filter((v) => v !== null) + .sort((a, b) => a - b) + if (indices.length >= 2) { + indices.unshift(min) + indices.push(max) + } + return { + name: paramName, + min, + max, + isLog: distribution.log, + isCat: false, + indices, + values, + } +} + export const makeHovertext = (trial: Trial): string => { return JSON.stringify( { From 0df7ffa0f59f97127f8ef3673ea4847ee27f0397 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Sun, 5 Nov 2023 16:54:48 +0900 Subject: [PATCH 082/176] Use implementation in graphUtil for rank plot --- optuna_dashboard/ts/components/GraphRank.tsx | 103 +++---------------- optuna_dashboard/ts/graphUtil.ts | 2 +- 2 files changed, 13 insertions(+), 92 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index 6781793f..f39def0e 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -12,19 +12,11 @@ import { Box, } from "@mui/material" import { plotlyDarkTemplate } from "./PlotlyDarkMode" -import { makeHovertext } from "../graphUtil" +import { AxisInfo, getAxisInfo, makeHovertext } from "../graphUtil" import { useMergedUnionSearchSpace } from "../searchSpace" -const PADDING_RATIO = 0.05 const plotDomId = "graph-rank" -interface AxisInfo { - name: string - range: [number, number] - isLog: boolean - isCat: boolean -} - interface RankPlotInfo { xaxis: AxisInfo yaxis: AxisInfo @@ -159,23 +151,18 @@ const getRankPlotInfo = ( const zValues: number[] = [] const isFeasible: boolean[] = [] const hovertext: string[] = [] - filteredTrials.forEach((trial) => { - const xValue = - trial.params.find((p) => p.name === xAxis.name)?.param_external_value || - null - const yValue = - trial.params.find((p) => p.name === yAxis.name)?.param_external_value || - null - if (trial.values === undefined || xValue === null || yValue === null) { - return + filteredTrials.forEach((trial, i) => { + if (xAxis.values[i] && yAxis.values[i] && trial.values) { + const xValue = xAxis.values[i] as string | number + const yValue = yAxis.values[i] as string | number + xValues.push(xValue) + yValues.push(yValue) + const zValue = Number(trial.values[objectiveId]) + zValues.push(zValue) + const feasibility = trial.constraints.every((c) => c <= 0) + isFeasible.push(feasibility) + hovertext.push(makeHovertext(trial)) } - const zValue = Number(trial.values[objectiveId]) - const feasibility = trial.constraints.every((c) => c <= 0) - xValues.push(xValue) - yValues.push(yValue) - zValues.push(zValue) - isFeasible.push(feasibility) - hovertext.push(makeHovertext(trial)) }) const colors = getColors(zValues) @@ -196,72 +183,6 @@ const filterFunc = (trial: Trial): boolean => { return trial.state === "Complete" && trial.values !== undefined } -const getAxisInfo = (trials: Trial[], param: SearchSpaceItem): AxisInfo => { - if (param.distribution.type === "CategoricalDistribution") { - return getAxisInfoForCategorical(trials, param.name, param.distribution) - } else { - return getAxisInfoForNumerical(trials, param.name, param.distribution) - } -} - -const getAxisInfoForCategorical = ( - trials: Trial[], - param: string, - distribution: CategoricalDistribution -): AxisInfo => { - const values = trials.map( - (trial) => - trial.params.find((p) => p.name === param)?.param_internal_value || null - ) - const isDynamic = values.some((v) => v === null) - const span = distribution.choices.length - (isDynamic ? 2 : 1) - const padding = span * PADDING_RATIO - const min = -padding - const max = span + padding - - return { - name: param, - range: [min, max], - isLog: false, - isCat: true, - } -} - -const getAxisInfoForNumerical = ( - trials: Trial[], - param: string, - distribution: FloatDistribution | IntDistribution -): AxisInfo => { - const values = trials.map( - (trial) => - trial.params.find((p) => p.name === param)?.param_internal_value || null - ) - const nonNullValues: number[] = [] - values.forEach((value) => { - if (value !== null) { - nonNullValues.push(value) - } - }) - let min = Math.min(...nonNullValues) - let max = Math.max(...nonNullValues) - if (distribution.log) { - const padding = (Math.log10(max) - Math.log10(min)) * PADDING_RATIO - min = Math.pow(10, Math.log10(min) - padding) - max = Math.pow(10, Math.log10(max) + padding) - } else { - const padding = (max - min) * PADDING_RATIO - min = min - padding - max = max + padding - } - - return { - name: param, - range: [min, max], - isLog: distribution.log, - isCat: false, - } -} - const getColors = (values: number[]): number[] => { const rawRanks = getOrderWithSameOrderAveraging(values) let colorIdxs: number[] = [] diff --git a/optuna_dashboard/ts/graphUtil.ts b/optuna_dashboard/ts/graphUtil.ts index 4e6f74e8..26f63971 100644 --- a/optuna_dashboard/ts/graphUtil.ts +++ b/optuna_dashboard/ts/graphUtil.ts @@ -1,6 +1,6 @@ const PADDING_RATIO = 0.05 -type AxisInfo = { +export type AxisInfo = { name: string min: number max: number From 55b2adf04fe2be8b3bff4d993746b629d9382cdb Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Sun, 5 Nov 2023 17:18:39 +0900 Subject: [PATCH 083/176] Delete min and max in AxisInfo --- optuna_dashboard/ts/graphUtil.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/optuna_dashboard/ts/graphUtil.ts b/optuna_dashboard/ts/graphUtil.ts index 26f63971..35a9002b 100644 --- a/optuna_dashboard/ts/graphUtil.ts +++ b/optuna_dashboard/ts/graphUtil.ts @@ -2,8 +2,6 @@ const PADDING_RATIO = 0.05 export type AxisInfo = { name: string - min: number - max: number isLog: boolean isCat: boolean indices: (string | number)[] @@ -42,11 +40,6 @@ const getAxisInfoForCategoricalParams = ( trial.params.find((p) => p.name === paramName)?.param_external_value || null ) - const isDynamic = values.some((v) => v === null) - const span = distribution.choices.length - (isDynamic ? 2 : 1) - const padding = span * PADDING_RATIO - const min = -padding - const max = span + padding const indices = distribution.choices .map((c) => c.value) @@ -59,8 +52,6 @@ const getAxisInfoForCategoricalParams = ( ) return { name: paramName, - min, - max, isLog: false, isCat: true, indices, @@ -101,8 +92,6 @@ const getAxisInfoForNumericalParams = ( } return { name: paramName, - min, - max, isLog: distribution.log, isCat: false, indices, From 7f78b6dc0c32864fa4484e2c283bce902951eabb Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Sun, 5 Nov 2023 17:34:12 +0900 Subject: [PATCH 084/176] Change the definition of RankPlotInfo --- optuna_dashboard/ts/components/GraphRank.tsx | 115 ++++++++++--------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index f39def0e..dc0a3842 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -12,14 +12,16 @@ import { Box, } from "@mui/material" import { plotlyDarkTemplate } from "./PlotlyDarkMode" -import { AxisInfo, getAxisInfo, makeHovertext } from "../graphUtil" +import { getAxisInfo, makeHovertext } from "../graphUtil" import { useMergedUnionSearchSpace } from "../searchSpace" const plotDomId = "graph-rank" interface RankPlotInfo { - xaxis: AxisInfo - yaxis: AxisInfo + xtitle: string + ytitle: string + xtype: plotly.AxisType + ytype: plotly.AxisType xvalues: (string | number)[] yvalues: (string | number)[] zvalues: number[] @@ -146,8 +148,8 @@ const getRankPlotInfo = ( const xAxis = getAxisInfo(filteredTrials, xParam) const yAxis = getAxisInfo(filteredTrials, yParam) - const xValues: (string | number)[] = [] - const yValues: (string | number)[] = [] + let xValues: (string | number)[] = [] + let yValues: (string | number)[] = [] const zValues: number[] = [] const isFeasible: boolean[] = [] const hovertext: string[] = [] @@ -167,9 +169,53 @@ const getRankPlotInfo = ( const colors = getColors(zValues) + if (xAxis.isCat && !yAxis.isCat) { + const xIndices: number[] = Array.from(Array(xValues.length).keys()).sort( + (a, b) => + xValues[a] + .toString() + .toLowerCase() + .localeCompare(xValues[b].toString().toLowerCase()) + ) + xValues = xIndices.map((i) => xValues[i]) + yValues = xIndices.map((i) => yValues[i]) + } + if (!xAxis.isCat && yAxis.isCat) { + const yIndices: number[] = Array.from(Array(yValues.length).keys()).sort( + (a, b) => + yValues[a] + .toString() + .toLowerCase() + .localeCompare(yValues[b].toString().toLowerCase()) + ) + xValues = yIndices.map((i) => xValues[i]) + yValues = yIndices.map((i) => yValues[i]) + } + if (xAxis.isCat && yAxis.isCat) { + const indices: number[] = Array.from(Array(xValues.length).keys()).sort( + (a, b) => { + const xComp = xValues[a] + .toString() + .toLowerCase() + .localeCompare(xValues[b].toString().toLowerCase()) + if (xComp !== 0) { + return xComp + } + return yValues[a] + .toString() + .toLowerCase() + .localeCompare(yValues[b].toString().toLowerCase()) + } + ) + xValues = indices.map((i) => xValues[i]) + yValues = indices.map((i) => yValues[i]) + } + return { - xaxis: xAxis, - yaxis: yAxis, + xtitle: xAxis.name, + ytitle: yAxis.name, + xtype: xAxis.isCat ? "category" : xAxis.isLog ? "log" : "linear", + ytype: yAxis.isCat ? "category" : yAxis.isLog ? "log" : "linear", xvalues: xValues, yvalues: yValues, zvalues: zValues, @@ -220,16 +266,14 @@ const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { return } - const xAxis = rankPlotInfo.xaxis - const yAxis = rankPlotInfo.yaxis const layout: Partial = { xaxis: { - title: xAxis.name, - type: xAxis.isCat ? "category" : xAxis.isLog ? "log" : "linear", + title: rankPlotInfo.xtitle, + type: rankPlotInfo.xtype, }, yaxis: { - title: yAxis.name, - type: yAxis.isCat ? "category" : yAxis.isLog ? "log" : "linear", + title: rankPlotInfo.ytitle, + type: rankPlotInfo.ytype, }, margin: { l: 50, @@ -241,49 +285,8 @@ const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { template: mode === "dark" ? plotlyDarkTemplate : {}, } - let xValues = rankPlotInfo.xvalues - let yValues = rankPlotInfo.yvalues - if (xAxis.isCat && !yAxis.isCat) { - const xIndices: number[] = Array.from(Array(xValues.length).keys()).sort( - (a, b) => - xValues[a] - .toString() - .toLowerCase() - .localeCompare(xValues[b].toString().toLowerCase()) - ) - xValues = xIndices.map((i) => xValues[i]) - yValues = xIndices.map((i) => yValues[i]) - } - if (!xAxis.isCat && yAxis.isCat) { - const yIndices: number[] = Array.from(Array(yValues.length).keys()).sort( - (a, b) => - yValues[a] - .toString() - .toLowerCase() - .localeCompare(yValues[b].toString().toLowerCase()) - ) - xValues = yIndices.map((i) => xValues[i]) - yValues = yIndices.map((i) => yValues[i]) - } - if (xAxis.isCat && yAxis.isCat) { - const indices: number[] = Array.from(Array(xValues.length).keys()).sort( - (a, b) => { - const xComp = xValues[a] - .toString() - .toLowerCase() - .localeCompare(xValues[b].toString().toLowerCase()) - if (xComp !== 0) { - return xComp - } - return yValues[a] - .toString() - .toLowerCase() - .localeCompare(yValues[b].toString().toLowerCase()) - } - ) - xValues = indices.map((i) => xValues[i]) - yValues = indices.map((i) => yValues[i]) - } + const xValues = rankPlotInfo.xvalues + const yValues = rankPlotInfo.yvalues const plotData: Partial[] = [ { From 571bce66ebb37d5e8a7f0f3e34b4662347b4366e Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Sun, 5 Nov 2023 17:46:45 +0900 Subject: [PATCH 085/176] Change the definition of RankPlotInfo --- optuna_dashboard/ts/components/GraphRank.tsx | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index dc0a3842..63d2dec5 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -24,7 +24,6 @@ interface RankPlotInfo { ytype: plotly.AxisType xvalues: (string | number)[] yvalues: (string | number)[] - zvalues: number[] colors: number[] is_feasible: boolean[] hovertext: string[] @@ -170,15 +169,9 @@ const getRankPlotInfo = ( const colors = getColors(zValues) if (xAxis.isCat && !yAxis.isCat) { - const xIndices: number[] = Array.from(Array(xValues.length).keys()).sort( - (a, b) => - xValues[a] - .toString() - .toLowerCase() - .localeCompare(xValues[b].toString().toLowerCase()) - ) - xValues = xIndices.map((i) => xValues[i]) - yValues = xIndices.map((i) => yValues[i]) + const indices = xAxis.indices + xValues = indices.map((i: number) => xValues[i]) + yValues = indices.map((i) => yValues[i]) } if (!xAxis.isCat && yAxis.isCat) { const yIndices: number[] = Array.from(Array(yValues.length).keys()).sort( @@ -218,7 +211,6 @@ const getRankPlotInfo = ( ytype: yAxis.isCat ? "category" : yAxis.isLog ? "log" : "linear", xvalues: xValues, yvalues: yValues, - zvalues: zValues, colors, is_feasible: isFeasible, hovertext, @@ -285,9 +277,6 @@ const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { template: mode === "dark" ? plotlyDarkTemplate : {}, } - const xValues = rankPlotInfo.xvalues - const yValues = rankPlotInfo.yvalues - const plotData: Partial[] = [ { type: "scatter", From e60663ff24447d9a749e616fb34994790ac2f222 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Sun, 12 Nov 2023 21:34:46 +0900 Subject: [PATCH 086/176] Follow review comments --- optuna_dashboard/ts/components/GraphRank.tsx | 28 +++++++++++--------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index 63d2dec5..56693e5d 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -153,9 +153,9 @@ const getRankPlotInfo = ( const isFeasible: boolean[] = [] const hovertext: string[] = [] filteredTrials.forEach((trial, i) => { - if (xAxis.values[i] && yAxis.values[i] && trial.values) { - const xValue = xAxis.values[i] as string | number - const yValue = yAxis.values[i] as string | number + const xValue = xAxis.values[i] + const yValue = yAxis.values[i] + if (xValue && yValue && trial.values) { xValues.push(xValue) yValues.push(yValue) const zValue = Number(trial.values[objectiveId]) @@ -169,22 +169,26 @@ const getRankPlotInfo = ( const colors = getColors(zValues) if (xAxis.isCat && !yAxis.isCat) { - const indices = xAxis.indices - xValues = indices.map((i: number) => xValues[i]) + const indices: number[] = Array.from(Array(xValues.length).keys()).sort( + (a, b) => + xValues[a] + .toString() + .toLowerCase() + .localeCompare(xValues[b].toString().toLowerCase()) + ) + xValues = indices.map((i) => xValues[i]) yValues = indices.map((i) => yValues[i]) - } - if (!xAxis.isCat && yAxis.isCat) { - const yIndices: number[] = Array.from(Array(yValues.length).keys()).sort( + } else if (!xAxis.isCat && yAxis.isCat) { + const indices: number[] = Array.from(Array(yValues.length).keys()).sort( (a, b) => yValues[a] .toString() .toLowerCase() .localeCompare(yValues[b].toString().toLowerCase()) ) - xValues = yIndices.map((i) => xValues[i]) - yValues = yIndices.map((i) => yValues[i]) - } - if (xAxis.isCat && yAxis.isCat) { + xValues = indices.map((i) => xValues[i]) + yValues = indices.map((i) => yValues[i]) + } else if (xAxis.isCat && yAxis.isCat) { const indices: number[] = Array.from(Array(xValues.length).keys()).sort( (a, b) => { const xComp = xValues[a] From 06695125afeac0d57fd8a9968aa031cba6bd6e41 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Sun, 12 Nov 2023 22:21:30 +0900 Subject: [PATCH 087/176] Introduce eqeqeq and fix some lines --- .eslintrc.js | 3 +- optuna_dashboard/ts/action.ts | 2 +- optuna_dashboard/ts/components/DataGrid.tsx | 2 +- .../ts/components/GraphIntermediateValues.tsx | 2 +- .../ts/components/GraphParallelCoordinate.tsx | 2 +- .../ts/components/StudyDetail.tsx | 2 +- .../ts/components/StudyHistory.tsx | 2 +- .../ts/components/TrialFormWidgets.tsx | 12 +- optuna_dashboard/ts/components/TrialList.tsx | 2 +- optuna_dashboard/ts/components/TrialTable.tsx | 2 +- optuna_dashboard/ts/dominatedTrials.ts | 2 +- optuna_dashboard/ts/state.ts | 2 +- package-lock.json | 1577 +++++++++-------- package.json | 6 +- standalone_app/src/components/DataGrid.tsx | 2 +- .../src/components/PlotIntermediateValues.tsx | 2 +- standalone_app/src/components/TrialTable.tsx | 2 +- standalone_app/src/sqlite3.ts | 8 +- 18 files changed, 840 insertions(+), 792 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 5b390797..1c8f2bb5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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', diff --git a/optuna_dashboard/ts/action.ts b/optuna_dashboard/ts/action.ts index 41afdadb..ee3bba15 100644 --- a/optuna_dashboard/ts/action.ts +++ b/optuna_dashboard/ts/action.ts @@ -154,7 +154,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] } } diff --git a/optuna_dashboard/ts/components/DataGrid.tsx b/optuna_dashboard/ts/components/DataGrid.tsx index 24e45dc7..2855f1f7 100644 --- a/optuna_dashboard/ts/components/DataGrid.tsx +++ b/optuna_dashboard/ts/components/DataGrid.tsx @@ -358,7 +358,7 @@ function stableSort( 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) diff --git a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx index 2d86e464..a06832d9 100644 --- a/optuna_dashboard/ts/components/GraphIntermediateValues.tsx +++ b/optuna_dashboard/ts/components/GraphIntermediateValues.tsx @@ -78,7 +78,7 @@ const plotIntermediateValue = ( t.state === "Pruned" && t.values && t.values.length > 0) || - t.state == "Running" + t.state === "Running" ) const plotData: Partial[] = filteredTrials.map((trial) => { const values = trial.intermediate_values.filter( diff --git a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx index 71f2b32d..b991ef94 100644 --- a/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx +++ b/optuna_dashboard/ts/components/GraphParallelCoordinate.tsx @@ -164,7 +164,7 @@ const plotCoordinate = ( return truncated .split("") .map((c, i) => { - return (i + 1) % breakLength == 0 ? c + "
" : c + return (i + 1) % breakLength === 0 ? c + "
" : c }) .join("") } diff --git a/optuna_dashboard/ts/components/StudyDetail.tsx b/optuna_dashboard/ts/components/StudyDetail.tsx index 663ff399..70550465 100644 --- a/optuna_dashboard/ts/components/StudyDetail.tsx +++ b/optuna_dashboard/ts/components/StudyDetail.tsx @@ -192,7 +192,7 @@ export const StudyDetail: FC<{ ) - } else if (page == "preferenceHistory") { + } else if (page === "preferenceHistory") { content = } diff --git a/optuna_dashboard/ts/components/StudyHistory.tsx b/optuna_dashboard/ts/components/StudyHistory.tsx index 907acd1a..e94a17ba 100644 --- a/optuna_dashboard/ts/components/StudyHistory.tsx +++ b/optuna_dashboard/ts/components/StudyHistory.tsx @@ -105,7 +105,7 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { {studyDetail !== null && - studyDetail.directions.length == 1 && + studyDetail.directions.length === 1 && studyDetail.has_intermediate_values ? ( { - 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): void => { e.preventDefault() const values = widgetStates.map((ws) => ws.value) - if (formWidgets.output_type == "objective") { + if (formWidgets.output_type === "objective") { const filtered = values.filter((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 } diff --git a/optuna_dashboard/ts/components/TrialList.tsx b/optuna_dashboard/ts/components/TrialList.tsx index 6922003a..cfde6536 100644 --- a/optuna_dashboard/ts/components/TrialList.tsx +++ b/optuna_dashboard/ts/components/TrialList.tsx @@ -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]) } diff --git a/optuna_dashboard/ts/components/TrialTable.tsx b/optuna_dashboard/ts/components/TrialTable.tsx index c708f6ee..0410818e 100644 --- a/optuna_dashboard/ts/components/TrialTable.tsx +++ b/optuna_dashboard/ts/components/TrialTable.tsx @@ -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", diff --git a/optuna_dashboard/ts/dominatedTrials.ts b/optuna_dashboard/ts/dominatedTrials.ts index 06afb15f..b1571775 100644 --- a/optuna_dashboard/ts/dominatedTrials.ts +++ b/optuna_dashboard/ts/dominatedTrials.ts @@ -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) => { diff --git a/optuna_dashboard/ts/state.ts b/optuna_dashboard/ts/state.ts index 3c6f0fd7..94e8d2d7 100644 --- a/optuna_dashboard/ts/state.ts +++ b/optuna_dashboard/ts/state.ts @@ -63,7 +63,7 @@ export const useStudyDetailValue = (studyId: number): StudyDetail | null => { export const useStudySummaryValue = (studyId: number): StudySummary | null => { const studySummaries = useRecoilValue(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 => { diff --git a/package-lock.json b/package-lock.json index 0561a75a..8aae1fb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,12 +43,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", @@ -61,6 +61,15 @@ "webpack-cli": "^4.9.2" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -1934,30 +1943,63 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "dependencies": { "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", - "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1969,13 +2011,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "engines": { - "node": ">= 4" + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { @@ -1990,24 +2035,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/js": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", + "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.0", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" }, "engines": { "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { @@ -4276,9 +4343,9 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/katex": { @@ -4409,6 +4476,12 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, + "node_modules/@types/semver": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", + "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -4470,30 +4543,33 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", - "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.10.0.tgz", + "integrity": "sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==", "dev": true, "dependencies": { - "@typescript-eslint/experimental-utils": "4.33.0", - "@typescript-eslint/scope-manager": "4.33.0", - "debug": "^4.3.1", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.1.8", - "regexpp": "^3.1.0", - "semver": "^7.3.5", - "tsutils": "^3.21.0" + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/type-utils": "6.10.0", + "@typescript-eslint/utils": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^4.0.0", - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4502,9 +4578,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4516,50 +4592,27 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", - "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", - "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.10.0.tgz", + "integrity": "sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "debug": "^4.3.1" + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/typescript-estree": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", + "debug": "^4.3.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4568,29 +4621,56 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", - "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz", + "integrity": "sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0" + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0" }, "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.10.0.tgz", + "integrity": "sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.10.0", + "@typescript-eslint/utils": "6.10.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@typescript-eslint/types": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", - "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.10.0.tgz", + "integrity": "sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==", "dev": true, "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4598,21 +4678,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", - "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz", + "integrity": "sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0", - "debug": "^4.3.1", - "globby": "^11.0.3", - "is-glob": "^4.0.1", - "semver": "^7.3.5", - "tsutils": "^3.21.0" + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4625,9 +4705,49 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.10.0.tgz", + "integrity": "sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/typescript-estree": "6.10.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4640,22 +4760,28 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", - "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz", + "integrity": "sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "4.33.0", - "eslint-visitor-keys": "^2.0.0" + "@typescript-eslint/types": "6.10.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@use-gesture/core": { "version": "10.2.27", "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.2.27.tgz", @@ -4983,15 +5109,6 @@ "ajv": "^6.9.1" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -5067,15 +5184,6 @@ "node": ">=8" } }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -6083,18 +6191,6 @@ "node": ">=10.13.0" } }, - "node_modules/enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/entities": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", @@ -6623,57 +6719,55 @@ } }, "node_modules/eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", + "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", "dev": true, "dependencies": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.53.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6692,40 +6786,16 @@ "node": ">=8.0.0" } }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint/node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.10.4" + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/ansi-styles": { @@ -6743,6 +6813,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6789,34 +6865,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { - "eslint-visitor-keys": "^1.1.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "engines": { - "node": ">=4" + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/globals": { - "version": "13.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", - "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -6837,28 +6930,61 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" + "argparse": "^2.0.1" }, "bin": { - "semver": "bin/semver.js" + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/supports-color": { @@ -6894,26 +7020,32 @@ } }, "node_modules/espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "node_modules/espree/node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true, + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=4" + "node": ">=0.4.0" } }, "node_modules/esprima": { @@ -6929,9 +7061,9 @@ } }, "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -7056,9 +7188,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -7071,6 +7203,18 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -7092,9 +7236,9 @@ } }, "node_modules/fastq": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", - "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -7169,22 +7313,23 @@ } }, "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", "dev": true, "dependencies": { - "flatted": "^3.1.0", + "flatted": "^3.2.9", + "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=12.0.0" } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, "node_modules/follow-redirects": { @@ -7261,12 +7406,6 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true - }, "node_modules/functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -7350,15 +7489,15 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/glob-to-regexp": { @@ -7427,6 +7566,12 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/hamt_plus": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", @@ -7821,9 +7966,9 @@ } }, "node_modules/ignore": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", - "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true, "engines": { "node": ">= 4" @@ -8123,6 +8268,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -10939,6 +11093,12 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -10968,6 +11128,15 @@ "node": ">=6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -11096,12 +11265,6 @@ "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==" }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -12317,17 +12480,17 @@ } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -12687,15 +12850,6 @@ "node": ">=6" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -13082,18 +13236,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/regexpu-core": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", @@ -13628,56 +13770,6 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -13917,44 +14009,6 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, - "node_modules/table": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", - "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", - "dev": true, - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/table/node_modules/ajv": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", - "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -14201,6 +14255,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-jest": { "version": "29.0.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", @@ -14363,27 +14429,6 @@ "node": ">=8" } }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -14681,12 +14726,6 @@ "node": ">=6" } }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "node_modules/v8-to-istanbul": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", @@ -15304,6 +15343,12 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -16624,37 +16669,61 @@ "dev": true, "optional": true }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true + }, "@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "requires": { "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "globals": { - "version": "13.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", - "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } }, "type-fest": { "version": "0.20.2", @@ -16664,21 +16733,33 @@ } } }, + "@eslint/js": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", + "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "dev": true + }, "@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.0", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" } }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "@istanbuljs/load-nyc-config": { @@ -18333,9 +18414,9 @@ } }, "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "@types/katex": { @@ -18468,6 +18549,12 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, + "@types/semver": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", + "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -18529,25 +18616,28 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", - "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.10.0.tgz", + "integrity": "sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.33.0", - "@typescript-eslint/scope-manager": "4.33.0", - "debug": "^4.3.1", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.1.8", - "regexpp": "^3.1.0", - "semver": "^7.3.5", - "tsutils": "^3.21.0" + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/type-utils": "6.10.0", + "@typescript-eslint/utils": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "dependencies": { "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -18555,67 +18645,92 @@ } } }, - "@typescript-eslint/experimental-utils": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", - "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - } - }, "@typescript-eslint/parser": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", - "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.10.0.tgz", + "integrity": "sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "debug": "^4.3.1" + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/typescript-estree": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", + "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", - "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz", + "integrity": "sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0" + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.10.0.tgz", + "integrity": "sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "6.10.0", + "@typescript-eslint/utils": "6.10.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", - "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.10.0.tgz", + "integrity": "sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", - "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz", + "integrity": "sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0", - "debug": "^4.3.1", - "globby": "^11.0.3", - "is-glob": "^4.0.1", - "semver": "^7.3.5", - "tsutils": "^3.21.0" + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "dependencies": { "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.10.0.tgz", + "integrity": "sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/typescript-estree": "6.10.0", + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -18624,15 +18739,21 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", - "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz", + "integrity": "sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.33.0", - "eslint-visitor-keys": "^2.0.0" + "@typescript-eslint/types": "6.10.0", + "eslint-visitor-keys": "^3.4.1" } }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "@use-gesture/core": { "version": "10.2.27", "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.2.27.tgz", @@ -18914,12 +19035,6 @@ "dev": true, "requires": {} }, - "ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true - }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -18977,12 +19092,6 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -19733,15 +19842,6 @@ "tapable": "^2.2.0" } }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - } - }, "entities": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", @@ -20034,62 +20134,51 @@ } }, "eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", + "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", "dev": true, "requires": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.53.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "dependencies": { - "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -20099,6 +20188,12 @@ "color-convert": "^2.0.1" } }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -20130,27 +20225,36 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" } }, "globals": { - "version": "13.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", - "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -20162,19 +20266,40 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "requires": { - "lru-cache": "^6.0.0" + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" } }, "supports-color": { @@ -20204,19 +20329,10 @@ "estraverse": "^4.1.1" } }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - } - }, "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, "esm": { @@ -20225,20 +20341,20 @@ "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" }, "espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" }, "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true } } @@ -20249,9 +20365,9 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -20347,9 +20463,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -20357,6 +20473,17 @@ "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } } }, "fast-json-stable-stringify": { @@ -20377,9 +20504,9 @@ "dev": true }, "fastq": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", - "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -20441,19 +20568,20 @@ } }, "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", "dev": true, "requires": { - "flatted": "^3.1.0", + "flatted": "^3.2.9", + "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, "follow-redirects": { @@ -20503,12 +20631,6 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true - }, "functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -20565,12 +20687,12 @@ } }, "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "requires": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" } }, "glob-to-regexp": { @@ -20625,6 +20747,12 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "hamt_plus": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", @@ -20912,9 +21040,9 @@ "requires": {} }, "ignore": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", - "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true }, "import-fresh": { @@ -21103,6 +21231,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, "is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -23209,6 +23343,12 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -23232,6 +23372,15 @@ "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", "dev": true }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -23339,12 +23488,6 @@ "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==" }, - "lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true - }, "longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -24140,17 +24283,17 @@ } }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" } }, "p-limit": { @@ -24391,12 +24534,6 @@ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==" }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -24680,12 +24817,6 @@ "functions-have-names": "^1.2.2" } }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, "regexpu-core": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", @@ -25072,43 +25203,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -25289,39 +25383,6 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, - "table": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", - "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", - "dev": true, - "requires": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ajv": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.2.tgz", - "integrity": "sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - } - } - }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -25496,6 +25557,13 @@ "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==" }, + "ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "requires": {} + }, "ts-jest": { "version": "29.0.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", @@ -25595,21 +25663,6 @@ } } }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -25809,12 +25862,6 @@ } } }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "v8-to-istanbul": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", diff --git a/package.json b/package.json index a675a1ce..406a7b07 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/standalone_app/src/components/DataGrid.tsx b/standalone_app/src/components/DataGrid.tsx index 24e45dc7..2855f1f7 100644 --- a/standalone_app/src/components/DataGrid.tsx +++ b/standalone_app/src/components/DataGrid.tsx @@ -358,7 +358,7 @@ function stableSort( 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) diff --git a/standalone_app/src/components/PlotIntermediateValues.tsx b/standalone_app/src/components/PlotIntermediateValues.tsx index 4feb0b33..8418cba6 100644 --- a/standalone_app/src/components/PlotIntermediateValues.tsx +++ b/standalone_app/src/components/PlotIntermediateValues.tsx @@ -78,7 +78,7 @@ const plotIntermediateValue = ( t.state === "Pruned" && t.values && t.values.length > 0) || - t.state == "Running" + t.state === "Running" ) const plotData: Partial[] = filteredTrials.map((trial) => { const values = trial.intermediate_values.filter( diff --git a/standalone_app/src/components/TrialTable.tsx b/standalone_app/src/components/TrialTable.tsx index 97a04f3c..d8b4f163 100644 --- a/standalone_app/src/components/TrialTable.tsx +++ b/standalone_app/src/components/TrialTable.tsx @@ -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", diff --git a/standalone_app/src/sqlite3.ts b/standalone_app/src/sqlite3.ts index 3a04c163..1fbcff87 100644 --- a/standalone_app/src/sqlite3.ts +++ b/standalone_app/src/sqlite3.ts @@ -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 }) } From e10a53c2c94609d6691d8b221973b8be7e7540ba Mon Sep 17 00:00:00 2001 From: HideakiImamura Date: Mon, 13 Nov 2023 11:59:20 +0900 Subject: [PATCH 088/176] Skip the test in optuna==2.10.0 --- .github/workflows/e2e-dashboard-tests.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-dashboard-tests.yml b/.github/workflows/e2e-dashboard-tests.yml index c8912b9a..a0396976 100644 --- a/.github/workflows/e2e-dashboard-tests.yml +++ b/.github/workflows/e2e-dashboard-tests.yml @@ -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 From fba9cf13717d5f651c744ade3ae756a1b366f82c Mon Sep 17 00:00:00 2001 From: Contramundum Date: Mon, 13 Nov 2023 16:58:14 +0900 Subject: [PATCH 089/176] Fix audio artifact component --- .../ts/components/ArtifactCardMedia.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/optuna_dashboard/ts/components/ArtifactCardMedia.tsx b/optuna_dashboard/ts/components/ArtifactCardMedia.tsx index 6326cb16..282b7432 100644 --- a/optuna_dashboard/ts/components/ArtifactCardMedia.tsx +++ b/optuna_dashboard/ts/components/ArtifactCardMedia.tsx @@ -4,7 +4,7 @@ import { isThreejsArtifact, } from "./ThreejsArtifactViewer" import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile" -import { CardMedia } from "@mui/material" +import { CardMedia, Box } from "@mui/material" export const ArtifactCardMedia: FC<{ artifact: Artifact @@ -35,9 +35,18 @@ export const ArtifactCardMedia: FC<{ ) } else if (artifact.mimetype.startsWith("audio")) { return ( - + + + ) } else if (artifact.mimetype.startsWith("image")) { return ( From 7632683da3ad3aedd7569f2ec774e4da53f0ccd4 Mon Sep 17 00:00:00 2001 From: c-bata Date: Wed, 15 Nov 2023 16:48:17 +0900 Subject: [PATCH 090/176] Split DataGridHeaderColumn component --- optuna_dashboard/ts/components/DataGrid.tsx | 144 ++++++++++++-------- 1 file changed, 87 insertions(+), 57 deletions(-) diff --git a/optuna_dashboard/ts/components/DataGrid.tsx b/optuna_dashboard/ts/components/DataGrid.tsx index 24e45dc7..32cbf5f9 100644 --- a/optuna_dashboard/ts/components/DataGrid.tsx +++ b/optuna_dashboard/ts/components/DataGrid.tsx @@ -119,7 +119,7 @@ function DataGrid(props: { }) // Sorting - const createSortHandler = (columnId: number) => () => { + const createSortHandler = (columnId: number) => { const isAsc = orderBy === columnId && order === "asc" setOrder(isAsc ? "desc" : "asc") setOrderBy(columnId) @@ -135,20 +135,6 @@ function DataGrid(props: { const RootDiv = styled("div")({ width: "100%", }) - const HiddenSpan = styled("span")({ - border: 0, - clip: "rect(0 0 0 0)", - height: 1, - margin: -1, - overflow: "hidden", - padding: 0, - position: "absolute", - top: 20, - width: 1, - }) - const TableHeaderCellSpan = styled("span")({ - display: "inline-flex", - }) return ( @@ -161,48 +147,16 @@ function DataGrid(props: { {collapseBody ? : null} {columns.map((column, columnIdx) => ( - - - {column.sortable ? ( - - {column.label} - {orderBy === column.field ? ( - - {order === "desc" - ? "sorted descending" - : "sorted ascending"} - - ) : null} - - ) : ( - column.label - )} - {column.filterable ? ( - { - clearFilter(columnIdx) - }} - > - - - ) : null} - - + + key={column.label} + createSortHandler={createSortHandler} + column={column} + columnIdx={columnIdx} + orderBy={orderBy} + order={order} + clearFilter={clearFilter} + filtered={fieldAlreadyFiltered(columnIdx)} + /> ))} @@ -239,6 +193,82 @@ function DataGrid(props: { ) } +function DataGridHeaderColumn(props: { + column: DataGridColumn + columnIdx: number + orderBy: number + order: Order + dense?: boolean + createSortHandler: (columnIdx: number) => void + clearFilter: (columnIdx: number) => void + filtered: boolean +}) { + const { + column, + columnIdx, + dense, + orderBy, + order, + createSortHandler, + clearFilter, + filtered, + } = props + + const HiddenSpan = styled("span")({ + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 20, + width: 1, + }) + const TableHeaderCellSpan = styled("span")({ + display: "inline-flex", + }) + return ( + + + {column.sortable ? ( + { + createSortHandler(columnIdx) + }} + > + {column.label} + {orderBy === column.field ? ( + + {order === "desc" ? "sorted descending" : "sorted ascending"} + + ) : null} + + ) : ( + column.label + )} + {column.filterable ? ( + { + clearFilter(columnIdx) + }} + > + + + ) : null} + + + ) +} + function DataGridRow(props: { columns: DataGridColumn[] rowIndex: number From dcabe98d3dff15ccc56fd5bd0cbc64ed0bc2a6c6 Mon Sep 17 00:00:00 2001 From: c-bata Date: Thu, 16 Nov 2023 10:08:18 +0900 Subject: [PATCH 091/176] Add refactor changes on DataGrid --- optuna_dashboard/ts/components/DataGrid.tsx | 62 +++++++++------------ 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/optuna_dashboard/ts/components/DataGrid.tsx b/optuna_dashboard/ts/components/DataGrid.tsx index 32cbf5f9..41ecc4e4 100644 --- a/optuna_dashboard/ts/components/DataGrid.tsx +++ b/optuna_dashboard/ts/components/DataGrid.tsx @@ -92,10 +92,6 @@ function DataGrid(props: { setFilters(newFilters) } - const clearFilter = (columnIdx: number): void => { - setFilters(filters.filter((f) => f.columnIdx !== columnIdx)) - } - const filteredRows = rows.filter((row, rowIdx) => { if (defaultFilter !== undefined && defaultFilter(row)) { return false @@ -119,11 +115,6 @@ function DataGrid(props: { }) // Sorting - const createSortHandler = (columnId: number) => { - const isAsc = orderBy === columnId && order === "asc" - setOrder(isAsc ? "desc" : "asc") - setOrderBy(columnId) - } const sortedRows = stableSort(filteredRows, order, orderBy, columns) const currentPageRows = rowsPerPage > 0 @@ -149,12 +140,15 @@ function DataGrid(props: { {columns.map((column, columnIdx) => ( key={column.label} - createSortHandler={createSortHandler} column={column} - columnIdx={columnIdx} - orderBy={orderBy} - order={order} - clearFilter={clearFilter} + orderBy={orderBy === columnIdx ? order : null} + onOrderByChange={(direction: Order) => { + setOrder(direction) + setOrderBy(columnIdx) + }} + onFilterClear={() => { + setFilters(filters.filter((f) => f.columnIdx !== columnIdx)) + }} filtered={fieldAlreadyFiltered(columnIdx)} /> ))} @@ -195,24 +189,14 @@ function DataGrid(props: { function DataGridHeaderColumn(props: { column: DataGridColumn - columnIdx: number - orderBy: number - order: Order - dense?: boolean - createSortHandler: (columnIdx: number) => void - clearFilter: (columnIdx: number) => void + orderBy: Order | null + onOrderByChange: (direction: Order) => void filtered: boolean + onFilterClear: () => void + dense?: boolean }) { - const { - column, - columnIdx, - dense, - orderBy, - order, - createSortHandler, - clearFilter, - filtered, - } = props + const { column, orderBy, onOrderByChange, filtered, onFilterClear, dense } = + props const HiddenSpan = styled("span")({ border: 0, @@ -231,21 +215,25 @@ function DataGridHeaderColumn(props: { return ( {column.sortable ? ( { - createSortHandler(columnIdx) + if (orderBy === null) { + onOrderByChange("asc") + } else { + onOrderByChange(orderBy === "desc" ? "asc" : "desc") + } }} > {column.label} - {orderBy === column.field ? ( + {orderBy !== null ? ( - {order === "desc" ? "sorted descending" : "sorted ascending"} + {orderBy === "desc" ? "sorted descending" : "sorted ascending"} ) : null} @@ -258,7 +246,7 @@ function DataGridHeaderColumn(props: { style={filtered ? {} : { visibility: "hidden" }} color="inherit" onClick={() => { - clearFilter(columnIdx) + onFilterClear() }} > From aa4909015ae27e4785493d85eb9306673b46fe45 Mon Sep 17 00:00:00 2001 From: c-bata Date: Thu, 16 Nov 2023 11:06:31 +0900 Subject: [PATCH 092/176] Filter DataGrid rows by multiple conditions --- optuna_dashboard/ts/components/DataGrid.tsx | 222 +++++++++--------- optuna_dashboard/ts/components/TrialTable.tsx | 10 +- 2 files changed, 121 insertions(+), 111 deletions(-) diff --git a/optuna_dashboard/ts/components/DataGrid.tsx b/optuna_dashboard/ts/components/DataGrid.tsx index 41ecc4e4..64f5303b 100644 --- a/optuna_dashboard/ts/components/DataGrid.tsx +++ b/optuna_dashboard/ts/components/DataGrid.tsx @@ -10,12 +10,16 @@ import { TableSortLabel, Collapse, IconButton, - useTheme, + Menu, + MenuItem, } from "@mui/material" import { styled } from "@mui/system" import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown" import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp" -import { Clear } from "@mui/icons-material" +import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank" +import CheckBoxIcon from "@mui/icons-material/CheckBox" +import FilterListIcon from "@mui/icons-material/FilterList" +import ListItemIcon from "@mui/material/ListItemIcon" type Order = "asc" | "desc" @@ -29,14 +33,14 @@ interface DataGridColumn { label: string sortable?: boolean less?: (a: T, b: T, ascending: boolean) => number - filterable?: boolean + filterChoices?: string[] toCellValue?: (rowIndex: number) => string | React.ReactNode padding?: "normal" | "checkbox" | "none" } interface RowFilter { columnIdx: number - value: Value + values: Value[] } function DataGrid(props: { @@ -81,24 +85,13 @@ function DataGrid(props: { } // Filtering - const fieldAlreadyFiltered = (columnIdx: number): boolean => - filters.some((f) => f.columnIdx === columnIdx) - - const handleClickFilterCell = (columnIdx: number, value: Value) => { - if (fieldAlreadyFiltered(columnIdx)) { - return - } - const newFilters = [...filters, { columnIdx: columnIdx, value: value }] - setFilters(newFilters) - } - const filteredRows = rows.filter((row, rowIdx) => { if (defaultFilter !== undefined && defaultFilter(row)) { return false } return filters.length === 0 ? true - : filters.some((f) => { + : filters.every((f) => { if (columns.length <= f.columnIdx) { console.log( `columnIdx=${f.columnIdx} must be smaller than columns.length=${columns.length}` @@ -106,11 +99,11 @@ function DataGrid(props: { return true } const toCellValue = columns[f.columnIdx].toCellValue - if (toCellValue !== undefined) { - return toCellValue(rowIdx) === f.value - } - const field = columns[f.columnIdx].field - return row[field] === f.value + const value = + toCellValue !== undefined + ? toCellValue(rowIdx) + : row[columns[f.columnIdx].field] + return f.values.some((v) => v === value) }) }) @@ -137,21 +130,32 @@ function DataGrid(props: { {collapseBody ? : null} - {columns.map((column, columnIdx) => ( - - key={column.label} - column={column} - orderBy={orderBy === columnIdx ? order : null} - onOrderByChange={(direction: Order) => { - setOrder(direction) - setOrderBy(columnIdx) - }} - onFilterClear={() => { - setFilters(filters.filter((f) => f.columnIdx !== columnIdx)) - }} - filtered={fieldAlreadyFiltered(columnIdx)} - /> - ))} + {columns.map((column, columnIdx) => { + return ( + + key={columnIdx} + column={column} + order={orderBy === columnIdx ? order : null} + filter={ + filters.find((f) => f.columnIdx === columnIdx) || null + } + onOrderByChange={(direction: Order) => { + setOrder(direction) + setOrderBy(columnIdx) + }} + onFilterChange={(values: Value[]) => { + const newFilters = filters.filter( + (f) => f.columnIdx !== columnIdx + ) + newFilters.push({ + columnIdx: columnIdx, + values: values, + }) + setFilters(newFilters) + }} + /> + ) + })} @@ -163,7 +167,6 @@ function DataGrid(props: { keyField={keyField} collapseBody={collapseBody} key={`${row[keyField]}`} - handleClickFilterCell={handleClickFilterCell} /> ))} {emptyRows > 0 && ( @@ -187,70 +190,103 @@ function DataGrid(props: { ) } +const TableHeaderCellSpan = styled("span")({ + display: "inline-flex", +}) + +const HiddenSpan = styled("span")({ + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 20, + width: 1, +}) + function DataGridHeaderColumn(props: { column: DataGridColumn - orderBy: Order | null - onOrderByChange: (direction: Order) => void - filtered: boolean - onFilterClear: () => void + order: Order | null + onOrderByChange: (order: Order) => void + filter: RowFilter | null + onFilterChange: (values: Value[]) => void dense?: boolean }) { - const { column, orderBy, onOrderByChange, filtered, onFilterClear, dense } = + const { column, order, onOrderByChange, filter, onFilterChange, dense } = props + const [filterMenuAnchorEl, setFilterMenuAnchorEl] = + React.useState(null) + + const filterChoices = column.filterChoices - const HiddenSpan = styled("span")({ - border: 0, - clip: "rect(0 0 0 0)", - height: 1, - margin: -1, - overflow: "hidden", - padding: 0, - position: "absolute", - top: 20, - width: 1, - }) - const TableHeaderCellSpan = styled("span")({ - display: "inline-flex", - }) return ( {column.sortable ? ( { - if (orderBy === null) { - onOrderByChange("asc") - } else { - onOrderByChange(orderBy === "desc" ? "asc" : "desc") - } + onOrderByChange(order === "asc" ? "desc" : "asc") }} > {column.label} - {orderBy !== null ? ( + {order !== null ? ( - {orderBy === "desc" ? "sorted descending" : "sorted ascending"} + {order === "desc" ? "sorted descending" : "sorted ascending"} ) : null} ) : ( column.label )} - {column.filterable ? ( - { - onFilterClear() - }} - > - - + {filterChoices !== undefined ? ( + <> + { + setFilterMenuAnchorEl(e.currentTarget) + }} + > + + + { + setFilterMenuAnchorEl(null) + }} + > + {filterChoices.map((choice, i) => ( + { + const values = + filter === null + ? filterChoices.filter((v) => v !== choice) + : filter.values.some((v) => v === choice) + ? filter.values.filter((v) => v !== choice) + : [...filter.values, choice] + onFilterChange(values) + }} + > + + {!filter || !filter.values.every((v) => v !== choice) ? ( + + ) : ( + + )} + + {choice} + + ))} + + ) : null} @@ -263,24 +299,10 @@ function DataGridRow(props: { row: T keyField: keyof T collapseBody?: (rowIndex: number) => React.ReactNode - handleClickFilterCell: (columnIdx: number, value: Value) => void }) { - const { - columns, - rowIndex, - row, - keyField, - collapseBody, - handleClickFilterCell, - } = props + const { columns, rowIndex, row, keyField, collapseBody } = props const [open, setOpen] = React.useState(false) - const theme = useTheme() - const FilterableDiv = styled("div")({ - color: theme.palette.primary.main, - textDecoration: "underline", - cursor: "pointer", - }) return ( @@ -301,21 +323,7 @@ function DataGridRow(props: { : // TODO(c-bata): Avoid this implicit type conversion. (row[column.field] as number | string | null | undefined) - return column.filterable ? ( - { - const value = - column.toCellValue !== undefined - ? column.toCellValue(rowIndex) - : row[column.field] - handleClickFilterCell(columnIndex, value) - }} - > - {cellItem} - - ) : ( + return ( trials[i].state.toString(), }, @@ -97,7 +97,10 @@ export const TrialTable: FC<{ ) { studyDetail?.intersection_search_space.forEach((s) => { const sortable = s.distribution.type !== "CategoricalDistribution" - const filterable = s.distribution.type === "CategoricalDistribution" + const filterChoices = + s.distribution.type === "CategoricalDistribution" + ? s.distribution.choices.map((c) => c.value) + : undefined columns.push({ field: "params", label: `Param ${s.name}`, @@ -105,7 +108,7 @@ export const TrialTable: FC<{ trials[i].params.find((p) => p.name === s.name) ?.param_external_value || null, sortable: sortable, - filterable: filterable, + filterChoices: filterChoices, // eslint-disable-next-line @typescript-eslint/no-unused-vars less: (firstEl, secondEl, _): number => { const firstVal = firstEl.params.find( @@ -146,7 +149,6 @@ export const TrialTable: FC<{ trials[i].user_attrs.find((attr) => attr.key === attr_spec.key) ?.value || null, sortable: attr_spec.sortable, - filterable: !attr_spec.sortable, // eslint-disable-next-line @typescript-eslint/no-unused-vars less: (firstEl, secondEl, _): number => { const firstVal = firstEl.user_attrs.find( From 075590240f8ffc78b3d7a7102dec07c00dbdfe18 Mon Sep 17 00:00:00 2001 From: Shinichi Hemmi <50256998+Alnusjaponica@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:59:41 +0900 Subject: [PATCH 093/176] make-order-consistent --- optuna_dashboard/ts/components/StudyDetail.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/ts/components/StudyDetail.tsx b/optuna_dashboard/ts/components/StudyDetail.tsx index 663ff399..f2867743 100644 --- a/optuna_dashboard/ts/components/StudyDetail.tsx +++ b/optuna_dashboard/ts/components/StudyDetail.tsx @@ -145,6 +145,8 @@ export const StudyDetail: FC<{ ) + } else if (page === "trialList") { + content = } else if (page === "trialTable") { content = ( @@ -153,8 +155,6 @@ export const StudyDetail: FC<{ ) - } else if (page === "trialList") { - content = } else if (page === "note" && studyDetail !== null) { content = ( Date: Thu, 16 Nov 2023 15:38:33 +0900 Subject: [PATCH 094/176] Fix broken tests --- optuna_dashboard/ts/components/DataGrid.tsx | 2 +- typescript_tests/DataGrid.test.tsx | 47 ++------------------- 2 files changed, 4 insertions(+), 45 deletions(-) diff --git a/optuna_dashboard/ts/components/DataGrid.tsx b/optuna_dashboard/ts/components/DataGrid.tsx index 64f5303b..faa7e75c 100644 --- a/optuna_dashboard/ts/components/DataGrid.tsx +++ b/optuna_dashboard/ts/components/DataGrid.tsx @@ -262,7 +262,7 @@ function DataGridHeaderColumn(props: { setFilterMenuAnchorEl(null) }} > - {filterChoices.map((choice, i) => ( + {filterChoices.map((choice) => ( { diff --git a/typescript_tests/DataGrid.test.tsx b/typescript_tests/DataGrid.test.tsx index 475e360a..6d13deb7 100644 --- a/typescript_tests/DataGrid.test.tsx +++ b/typescript_tests/DataGrid.test.tsx @@ -1,7 +1,7 @@ import React from "react" global.URL.createObjectURL = jest.fn() -import { cleanup, render, fireEvent } from "@testing-library/react" +import { cleanup, render } from "@testing-library/react" import { DataGrid, DataGridColumn, @@ -9,6 +9,7 @@ import { afterEach(cleanup) +// TODO(c-bata): Add tests to check filterChoices option it("Filter rows of DataGrid", () => { interface DummyAttribute { id: number @@ -23,7 +24,7 @@ it("Filter rows of DataGrid", () => { { id: 5, key: "foo", value: 3 }, ] const columns: DataGridColumn[] = [ - { field: "key", label: "Key", filterable: true }, + { field: "key", label: "Key" }, { field: "value", label: "Value", @@ -39,46 +40,4 @@ it("Filter rows of DataGrid", () => { /> ) expect(queryAllByText("bar").length).toBe(2) - - // Filter rows by "foo" - fireEvent.click(queryAllByText("foo")[0]) - expect(queryAllByText("foo").length).toBe(3) - expect(queryAllByText("bar").length).toBe(0) -}) - -it("Filter rows after sorted", () => { - interface DummyAttribute { - id: number - key: string - value: number - } - const dummyAttributes = [ - { id: 1, key: "foo", value: 4000 }, - { id: 2, key: "bar", value: 1000 }, - { id: 3, key: "bar", value: 2000 }, - { id: 4, key: "foo", value: 3000 }, - { id: 5, key: "foo", value: 5000 }, - ] - const columns: DataGridColumn[] = [ - { field: "key", label: "Key", filterable: true }, - { - field: "value", - label: "Value", - sortable: true, - }, - ] - - const { getByText, queryAllByText } = render( - - columns={columns} - rows={dummyAttributes} - keyField={"id"} - /> - ) - // Sort and filter rows - fireEvent.click(getByText("Value")) - fireEvent.click(queryAllByText("bar")[0]) - - expect(queryAllByText("1000").length).toBe(1) - expect(queryAllByText("2000").length).toBe(1) }) From 0b087b17d0201c743f996ab60eb6d0b672c5fb9c Mon Sep 17 00:00:00 2001 From: HideakiImamura Date: Fri, 17 Nov 2023 14:22:13 +0900 Subject: [PATCH 095/176] Add unittests for tell_trial --- python_tests/test_api.py | 119 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/python_tests/test_api.py b/python_tests/test_api.py index bde7838a..eb96adda 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -399,6 +399,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) + 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) + 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: From 9d7e10759e7e9dcffc45939e0cfdae6d38fdf4fd Mon Sep 17 00:00:00 2001 From: gen740 Date: Fri, 17 Nov 2023 14:53:56 +0900 Subject: [PATCH 096/176] Hide study artifact card when artifact is not enabled --- .../ts/components/StudyHistory.tsx | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/optuna_dashboard/ts/components/StudyHistory.tsx b/optuna_dashboard/ts/components/StudyHistory.tsx index dbe15427..bdfb225b 100644 --- a/optuna_dashboard/ts/components/StudyHistory.tsx +++ b/optuna_dashboard/ts/components/StudyHistory.tsx @@ -172,31 +172,31 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { - - - - - + + + - Study Artifacts - - {artifactEnabled && studyDetail !== null && ( + + Study Artifacts + - )} - - + + + - + )} ) } From 6d0fef3ff279cc239e8349158535eff1015f69a2 Mon Sep 17 00:00:00 2001 From: HideakiImamura Date: Fri, 17 Nov 2023 16:42:39 +0900 Subject: [PATCH 097/176] Use subtest --- python_tests/test_api.py | 54 ++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/python_tests/test_api.py b/python_tests/test_api.py index eb96adda..ce78da47 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -463,20 +463,20 @@ class APITestCase(TestCase): study = optuna.create_study(storage=storage) for state in ["Pruned", "Running", "Waiting", "Invalid"]: 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": state, - } - ), - content_type="application/json", - ) - self.assertEqual(status, 400) + 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() @@ -502,21 +502,21 @@ class APITestCase(TestCase): study = optuna.create_study(storage=storage) for values in [1.0, ["foo"]]: 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": values, - } - ), - content_type="application/json", - ) - self.assertEqual(status, 400) + 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): From 68e181e838e769fcfc7964add044d8cf9e636869 Mon Sep 17 00:00:00 2001 From: gen740 Date: Fri, 17 Nov 2023 16:53:00 +0900 Subject: [PATCH 098/176] Delete unnecessary dashboard_artifact system_attr deletion --- optuna_dashboard/artifact/_backend.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/optuna_dashboard/artifact/_backend.py b/optuna_dashboard/artifact/_backend.py index 55dc844c..9720124b 100644 --- a/optuna_dashboard/artifact/_backend.py +++ b/optuna_dashboard/artifact/_backend.py @@ -204,10 +204,6 @@ def register_artifact_route( return {"reason": "Cannot access to the artifacts."} artifact_store.remove(artifact_id) - # The artifact's metadata is stored in one of the following two locations: - storage.set_study_system_attr( - study_id, _dashboard_artifact_prefix(study_id) + artifact_id, json.dumps(None) - ) storage.set_study_system_attr( study_id, ARTIFACTS_ATTR_PREFIX + artifact_id, json.dumps(None) ) From bcf8728325934a4eb227f42c80a97613f64664d4 Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Fri, 17 Nov 2023 17:55:05 +0900 Subject: [PATCH 099/176] Add tests for save_trial_user_attrs --- python_tests/test_api.py | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/python_tests/test_api.py b/python_tests/test_api.py index ce78da47..8aa8fac2 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -220,6 +220,49 @@ 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() From c1c6adc3a6318d56d4b6292878313309ee5575ae Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Fri, 17 Nov 2023 18:03:23 +0900 Subject: [PATCH 100/176] Remove unnecessary empty lines. --- python_tests/test_api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/python_tests/test_api.py b/python_tests/test_api.py index 8aa8fac2..721bdb56 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -246,7 +246,6 @@ class APITestCase(TestCase): 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() @@ -262,7 +261,6 @@ class APITestCase(TestCase): 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() From 319448a7c167ceb7823f2c4c26ae9532ca6c723b Mon Sep 17 00:00:00 2001 From: Victoria A <52001888+adjeiv@users.noreply.github.com> Date: Sat, 18 Nov 2023 10:57:39 +0000 Subject: [PATCH 101/176] Tests, formatting --- optuna_dashboard/_app.py | 2 +- optuna_dashboard/_note.py | 16 ++++++-- python_tests/test_note.py | 77 +++++++++++++++++++++++++++------------ 3 files changed, 66 insertions(+), 29 deletions(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index 9ce820b7..32e2a431 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -164,7 +164,7 @@ def create_app( if new_study_summary is None: response.status = 500 return {"reason": "Failed to load the new study"} - + note.transfer_notes(storage, src_study, dst_study) storage.delete_study(src_study._study_id) response.status = 201 diff --git a/optuna_dashboard/_note.py b/optuna_dashboard/_note.py index e510ddfd..6107d56c 100644 --- a/optuna_dashboard/_note.py +++ b/optuna_dashboard/_note.py @@ -109,6 +109,7 @@ def note_str_key_prefix(trial_id: Optional[int]) -> str: return prefix return f"dashboard:{trial_id}:note_str:" + def transfer_notes(storage: BaseStorage, src_study: optuna.Study, dst_study: optuna.Study) -> None: system_attrs = storage.get_study_system_attrs(study_id=src_study._study_id) @@ -120,11 +121,12 @@ def transfer_notes(storage: BaseStorage, src_study: optuna.Study, dst_study: opt # Transfer individual trial notes for src_trial, dst_trial in zip(src_study.get_trials(), dst_study.get_trials()): transfer(src_trial._trial_id, dst_trial._trial_id) - + # Transfer study note NO_SRC_TRIAL, NO_DST_TRIAL = None, None transfer(NO_SRC_TRIAL, NO_DST_TRIAL) + def get_note_from_system_attrs(system_attrs: dict[str, Any], trial_id: Optional[int]) -> NoteType: if note_ver_key(trial_id) not in system_attrs: return { @@ -146,7 +148,10 @@ def version_is_incremented( db_note_ver = system_attrs.get(note_ver_key(trial_id), 0) return req_note_ver == db_note_ver + 1 -def all_trial_notes(storage: BaseStorage, study_id: int, trial_id: Optional[int]) -> dict[str, str]: + +def all_trial_notes( + storage: BaseStorage, study_id: int, trial_id: Optional[int] +) -> dict[str, str]: all_note_attrs: dict[str, str] = { key: value for key, value in storage.get_study_system_attrs(study_id).items() @@ -154,6 +159,7 @@ def all_trial_notes(storage: BaseStorage, study_id: int, trial_id: Optional[int] } return all_note_attrs + def save_note_with_version( storage: BaseStorage, study_id: int, trial_id: Optional[int], ver: int, body: str ) -> None: @@ -169,19 +175,21 @@ def save_note_with_version( for i in range(len(attrs), len(all_note_attrs)): storage.set_study_system_attr(study_id, f"{note_str_key_prefix(trial_id)}{i}", "") + def delete_study_notes(storage: BaseStorage, study_id): - study = storage.get_study_name_from_id(study_id) for trial in storage.get_all_trials(study_id): delete_notes(storage, study_id, trial._trial_id) - + delete_notes(storage, study_id, None) + def delete_notes(storage: BaseStorage, study_id: int, trial_id: Optional[int]) -> None: all_note_attrs = all_trial_notes(storage, study_id, trial_id) for i in range(len(all_note_attrs)): storage.set_study_system_attr(study_id, f"{note_str_key_prefix(trial_id)}{i}", "") + def split_body(note_str: str, trial_id: Optional[int]) -> dict[str, str]: note_len = len(note_str) attrs = {} diff --git a/python_tests/test_note.py b/python_tests/test_note.py index 4997d16a..55c8f610 100644 --- a/python_tests/test_note.py +++ b/python_tests/test_note.py @@ -56,41 +56,70 @@ class NoteTestCase(TestCase): def test_delete_notes_trial(self) -> None: study = optuna.create_study() - trial_1 = study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) - trial_2 = study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) + trials = [ + study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) for _ in range(2) + ] storage = study._storage - for trial, body in [(trial_1, "version 1"), (trial_2, "version 2")]: - save_note(trial, body) + notes = ["trial 0", "trial 1"] + for trial, body in zip(trials, notes): + with self.subTest(body): + save_note(trial, body) - # first assert existence - actual = get_note(trial) - self.assertEqual(actual, body) + self.assertEqual(get_note(trial), body) - # delete - note.delete_notes(storage, study._study_id, trial._trial_id) + note.delete_notes(storage, study._study_id, trial._trial_id) - # assert deletion - actual = get_note(trial) - self.assertEqual(actual, "") + self.assertEqual(get_note(trial), "") + def test_delete_notes_study(self) -> None: + study = optuna.create_study() + trials = [ + study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) for _ in range(2) + ] + storage = study._storage + + notes = ["trial 0", "trial 1"] + for trial, body in zip(trials, notes): + with self.subTest(body): + save_note(trial, body) + + actual = get_note(trial) + self.assertEqual(actual, body) + + save_note(study, "Study note") + actual = get_note(study) + self.assertEqual(actual, "Study note") note.delete_study_notes(storage, study._study_id) - def test_delete_notes_study(self) -> None: - pass + for trial in trials: + self.assertEqual(get_note(trial), "") + self.assertEqual(get_note(study), "") def test_transfer_notes(self) -> None: - study = optuna.create_study() - trial_1 = study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) - trial_2 = study.ask({"x2": optuna.distributions.FloatDistribution(0, 10)}) - storage = study._storage + old_study = optuna.create_study() + old_trials = [ + old_study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) for _ in range(2) + ] + storage = old_study._storage - save_note(trial_1, "trial 1") - save_note(trial_2, "trial 2") + notes = ["trial 0", "trial 1"] + for trial, body in zip(old_trials, notes): + save_note(trial, body) + save_note(old_study, "Study") - new_study = optuna.create_study( - storage=storage, directions=study.directions - ) - note.transfer_notes(storage, study, new_study) + new_study = optuna.create_study(storage=storage, directions=old_study.directions) + new_study.add_trials(old_study.get_trials(deepcopy=False)) + note.transfer_notes(storage, old_study, new_study) + + for old_trial in old_trials: + self.assertEqual(get_note(old_trial), "") + self.assertEqual(get_note(old_study), "") + + system_attrs = new_study._storage.get_study_system_attrs(new_study._study_id) + for new_trial, body in zip(new_study.get_trials(), notes): + actual = note.get_note_from_system_attrs(system_attrs, new_trial._trial_id) + self.assertEqual(actual["body"], body) + self.assertEqual(get_note(new_study), "Study") From 11e38ea439077d5aa13390cbfc8c2823d869341d Mon Sep 17 00:00:00 2001 From: Victoria A <52001888+adjeiv@users.noreply.github.com> Date: Sat, 18 Nov 2023 11:30:55 +0000 Subject: [PATCH 102/176] Typing info --- optuna_dashboard/_note.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/_note.py b/optuna_dashboard/_note.py index 6107d56c..57135211 100644 --- a/optuna_dashboard/_note.py +++ b/optuna_dashboard/_note.py @@ -176,7 +176,7 @@ def save_note_with_version( storage.set_study_system_attr(study_id, f"{note_str_key_prefix(trial_id)}{i}", "") -def delete_study_notes(storage: BaseStorage, study_id): +def delete_study_notes(storage: BaseStorage, study_id: int) -> None: for trial in storage.get_all_trials(study_id): delete_notes(storage, study_id, trial._trial_id) From ecf5ab80f7ed3a858b51640ec75e886dd06b828f Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Wed, 22 Nov 2023 11:48:22 +0900 Subject: [PATCH 103/176] Support numpy scalars in Trial.user_attrs --- .../_cached_extra_study_property.py | 11 ++++---- optuna_dashboard/_serializer.py | 2 ++ .../test_cached_extra_study_property.py | 23 ++++++++++++++-- python_tests/test_serializers.py | 27 +++++++++++++++++++ 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/optuna_dashboard/_cached_extra_study_property.py b/optuna_dashboard/_cached_extra_study_property.py index 2f27fa64..1e4fb1a7 100644 --- a/optuna_dashboard/_cached_extra_study_property.py +++ b/optuna_dashboard/_cached_extra_study_property.py @@ -2,12 +2,14 @@ from __future__ import annotations import copy import threading +from typing import Any from typing import List from typing import Optional from typing import Set from typing import Tuple from typing import TYPE_CHECKING +import numpy as np from optuna.distributions import BaseDistribution from optuna.trial import FrozenTrial from optuna.trial import TrialState @@ -84,12 +86,11 @@ class _CachedExtraStudyProperty: self._cursor = next_cursor + def _is_sortable_value(self, v: Any) -> bool: + return not isinstance(v, bool) and isinstance(v, (int, float, np.integer, np.floating)) + 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)) - for k, v in trial.user_attrs.items() - } + current_user_attrs = {k: self._is_sortable_value(v) for k, v in trial.user_attrs.items()} for attr_name, current_is_sortable in current_user_attrs.items(): is_sortable = self._union_user_attrs.get(attr_name) if is_sortable is None: diff --git a/optuna_dashboard/_serializer.py b/optuna_dashboard/_serializer.py index b5a3b305..9b0cce64 100644 --- a/optuna_dashboard/_serializer.py +++ b/optuna_dashboard/_serializer.py @@ -104,6 +104,8 @@ def serialize_attrs(attrs: dict[str, Any]) -> list[Attribute]: value = "" elif isinstance(v, str): value = v + elif isinstance(v, (np.floating, np.integer)): + value = v.item() else: value = json.dumps(v) value = value[:MAX_ATTR_LENGTH] if len(value) > MAX_ATTR_LENGTH else value diff --git a/python_tests/test_cached_extra_study_property.py b/python_tests/test_cached_extra_study_property.py index c02006a7..dcd5bc5e 100644 --- a/python_tests/test_cached_extra_study_property.py +++ b/python_tests/test_cached_extra_study_property.py @@ -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: diff --git a/python_tests/test_serializers.py b/python_tests/test_serializers.py index 0d1546d2..8c64c5d8 100644 --- a/python_tests/test_serializers.py +++ b/python_tests/test_serializers.py @@ -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() From 5810ef7760a12164f480e10e5f4f2ed2858a458d Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Wed, 22 Nov 2023 12:08:35 +0900 Subject: [PATCH 104/176] Cast to str. --- optuna_dashboard/_serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/_serializer.py b/optuna_dashboard/_serializer.py index 9b0cce64..2730c328 100644 --- a/optuna_dashboard/_serializer.py +++ b/optuna_dashboard/_serializer.py @@ -105,7 +105,7 @@ def serialize_attrs(attrs: dict[str, Any]) -> list[Attribute]: elif isinstance(v, str): value = v elif isinstance(v, (np.floating, np.integer)): - value = v.item() + value = str(v.item()) else: value = json.dumps(v) value = value[:MAX_ATTR_LENGTH] if len(value) > MAX_ATTR_LENGTH else value From 0aec86dbb1db6fe50bb80df8dea4c633947efca2 Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Wed, 22 Nov 2023 13:30:02 +0900 Subject: [PATCH 105/176] Fix expected values. --- python_tests/test_serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python_tests/test_serializers.py b/python_tests/test_serializers.py index 8c64c5d8..d1bdf59b 100644 --- a/python_tests/test_serializers.py +++ b/python_tests/test_serializers.py @@ -36,7 +36,7 @@ def test_serialize_numpy_integer() -> None: } ) assert len(serialized) == 4 - assert all([v["value"] == 1 for v in serialized]) + assert all([v["value"] == "1" for v in serialized]) def test_serialize_numpy_floating() -> None: @@ -49,7 +49,7 @@ def test_serialize_numpy_floating() -> None: } ) assert len(serialized) == 4 - assert all([v["value"] == 1.0 for v in serialized]) + assert all([v["value"] == "1.0" for v in serialized]) @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") From 7401fae2d66e2e5b73f298293bb34e056582f821 Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Wed, 22 Nov 2023 13:59:40 +0900 Subject: [PATCH 106/176] Check if value is numbers.Real --- optuna_dashboard/_cached_extra_study_property.py | 10 +++++----- optuna_dashboard/_serializer.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/optuna_dashboard/_cached_extra_study_property.py b/optuna_dashboard/_cached_extra_study_property.py index 1e4fb1a7..919853f4 100644 --- a/optuna_dashboard/_cached_extra_study_property.py +++ b/optuna_dashboard/_cached_extra_study_property.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +import numbers import threading from typing import Any from typing import List @@ -9,7 +10,6 @@ from typing import Set from typing import Tuple from typing import TYPE_CHECKING -import numpy as np from optuna.distributions import BaseDistribution from optuna.trial import FrozenTrial from optuna.trial import TrialState @@ -86,11 +86,11 @@ class _CachedExtraStudyProperty: self._cursor = next_cursor - def _is_sortable_value(self, v: Any) -> bool: - return not isinstance(v, bool) and isinstance(v, (int, float, np.integer, np.floating)) - def _update_user_attrs(self, trial: FrozenTrial) -> None: - current_user_attrs = {k: self._is_sortable_value(v) for k, v in trial.user_attrs.items()} + current_user_attrs = { + 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(): is_sortable = self._union_user_attrs.get(attr_name) if is_sortable is None: diff --git a/optuna_dashboard/_serializer.py b/optuna_dashboard/_serializer.py index 2730c328..dbcdc991 100644 --- a/optuna_dashboard/_serializer.py +++ b/optuna_dashboard/_serializer.py @@ -2,11 +2,11 @@ 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 -import numpy as np from optuna.distributions import BaseDistribution from optuna.distributions import CategoricalDistribution from optuna.study import StudySummary @@ -104,8 +104,8 @@ def serialize_attrs(attrs: dict[str, Any]) -> list[Attribute]: value = "" elif isinstance(v, str): value = v - elif isinstance(v, (np.floating, np.integer)): - value = str(v.item()) + 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 From f2f5e3665728465d89fafbbd4b34074ecb75490f Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Wed, 22 Nov 2023 06:40:15 +0100 Subject: [PATCH 107/176] Add tests for save_trial_note --- python_tests/test_api.py | 56 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/python_tests/test_api.py b/python_tests/test_api.py index 721bdb56..6c618816 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -9,6 +9,7 @@ from optuna import get_all_study_summaries from optuna.study import StudyDirection from optuna_dashboard._app import create_app from optuna_dashboard._app import create_new_study +from optuna_dashboard._note import note_str_key_prefix, note_ver_key from optuna_dashboard._preference_setting import register_preference_feedback_component from optuna_dashboard._preferential_history import NewHistory from optuna_dashboard._preferential_history import remove_history @@ -261,6 +262,61 @@ class APITestCase(TestCase): self.assertEqual(status, 400) assert study.trials[0].user_attrs == {} + def _test_save_trial_note( + self, request_body: dict[str, int | str] + ) -> tuple[int, optuna.Study]: + study = optuna.create_study() + trial = study.ask() + app = create_app(study._storage) + status, _, _ = send_request( + app, + f"/api/studies/{study._study_id}/{trial._trial_id}/note", + "PUT", + content_type="application/json", + body=json.dumps(request_body), + ) + return status, study + + def test_save_trial_note_overwrite(self) -> None: + study = optuna.create_study() + trial = study.ask() + app = create_app(study._storage) + for ver in range(1, 3): + request_body = {"body": f"Test note ver. {ver}.", "version": ver} + status, _, _ = send_request( + app, + f"/api/studies/{study._study_id}/{trial._trial_id}/note", + "PUT", + content_type="application/json", + body=json.dumps(request_body), + ) + self.assertEqual(status, 204) + # Check if the version 1 is deleted. + assert study.system_attrs == { + note_ver_key(0): request_body["version"], + f"{note_str_key_prefix(0)}{0}": request_body["body"], + } + + def test_save_trial_note(self) -> None: + request_body = {"body": "Test note.", "version": 1} + status, study = self._test_save_trial_note(request_body) + self.assertEqual(status, 204) + assert study.system_attrs == { + note_ver_key(0): request_body["version"], + f"{note_str_key_prefix(0)}{0}": request_body["body"], + } + + def test_save_trial_note_with_wrong_version(self) -> None: + request_body = {"body": "Test note.", "version": 0} + status, study = self._test_save_trial_note(request_body) + self.assertEqual(status, 409) + assert study.system_attrs == {} + + def test_save_trial_note_empty(self) -> None: + status, study = self._test_save_trial_note(request_body={}) + self.assertEqual(status, 400) + assert study.system_attrs == {} + @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_skip_trial(self) -> None: storage = optuna.storages.InMemoryStorage() From f0f876596a1ad1e98d8016a0d03732a1566c3c98 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Wed, 22 Nov 2023 06:47:33 +0100 Subject: [PATCH 108/176] Apply flake8 --- python_tests/test_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python_tests/test_api.py b/python_tests/test_api.py index 6c618816..1bae1872 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -294,7 +294,7 @@ class APITestCase(TestCase): # Check if the version 1 is deleted. assert study.system_attrs == { note_ver_key(0): request_body["version"], - f"{note_str_key_prefix(0)}{0}": request_body["body"], + f"{note_str_key_prefix(0)}{0}": request_body["body"], } def test_save_trial_note(self) -> None: @@ -303,7 +303,7 @@ class APITestCase(TestCase): self.assertEqual(status, 204) assert study.system_attrs == { note_ver_key(0): request_body["version"], - f"{note_str_key_prefix(0)}{0}": request_body["body"], + f"{note_str_key_prefix(0)}{0}": request_body["body"], } def test_save_trial_note_with_wrong_version(self) -> None: From dcbd7a3c982694cfc01bcba9c305026675203173 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Wed, 22 Nov 2023 06:52:06 +0100 Subject: [PATCH 109/176] Apply formatter --- python_tests/test_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python_tests/test_api.py b/python_tests/test_api.py index 1bae1872..e62ebba0 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -9,7 +9,8 @@ from optuna import get_all_study_summaries from optuna.study import StudyDirection from optuna_dashboard._app import create_app from optuna_dashboard._app import create_new_study -from optuna_dashboard._note import note_str_key_prefix, note_ver_key +from optuna_dashboard._note import note_str_key_prefix +from optuna_dashboard._note import note_ver_key from optuna_dashboard._preference_setting import register_preference_feedback_component from optuna_dashboard._preferential_history import NewHistory from optuna_dashboard._preferential_history import remove_history From 685e2846135eccfea41fc73b867ca94a961479c5 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Wed, 22 Nov 2023 06:54:55 +0100 Subject: [PATCH 110/176] Apply mypy --- python_tests/test_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python_tests/test_api.py b/python_tests/test_api.py index e62ebba0..0250ad66 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -299,7 +299,7 @@ class APITestCase(TestCase): } def test_save_trial_note(self) -> None: - request_body = {"body": "Test note.", "version": 1} + request_body: dict[str, int | str] = {"body": "Test note.", "version": 1} status, study = self._test_save_trial_note(request_body) self.assertEqual(status, 204) assert study.system_attrs == { @@ -308,7 +308,7 @@ class APITestCase(TestCase): } def test_save_trial_note_with_wrong_version(self) -> None: - request_body = {"body": "Test note.", "version": 0} + request_body: dict[str, int | str] = {"body": "Test note.", "version": 0} status, study = self._test_save_trial_note(request_body) self.assertEqual(status, 409) assert study.system_attrs == {} From ad4d29f70b2f6c2c7e6fbca52f75b9264ac011ae Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Wed, 22 Nov 2023 15:22:23 +0900 Subject: [PATCH 111/176] Fix import lines --- optuna_dashboard/_cached_extra_study_property.py | 1 - optuna_dashboard/_serializer.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/_cached_extra_study_property.py b/optuna_dashboard/_cached_extra_study_property.py index 919853f4..24e22372 100644 --- a/optuna_dashboard/_cached_extra_study_property.py +++ b/optuna_dashboard/_cached_extra_study_property.py @@ -3,7 +3,6 @@ from __future__ import annotations import copy import numbers import threading -from typing import Any from typing import List from typing import Optional from typing import Set diff --git a/optuna_dashboard/_serializer.py b/optuna_dashboard/_serializer.py index dbcdc991..7030abec 100644 --- a/optuna_dashboard/_serializer.py +++ b/optuna_dashboard/_serializer.py @@ -7,6 +7,7 @@ from typing import Any from typing import TYPE_CHECKING from typing import Union +import numpy as np from optuna.distributions import BaseDistribution from optuna.distributions import CategoricalDistribution from optuna.study import StudySummary From d2748eba49e7e946e61d886d3348dc274408b7e7 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Thu, 23 Nov 2023 17:48:37 +0900 Subject: [PATCH 112/176] Fix errors --- optuna_dashboard/ts/components/GraphRank.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index 56693e5d..0ff215d1 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -281,6 +281,9 @@ const plotRank = (rankPlotInfo: RankPlotInfo | null, mode: string) => { template: mode === "dark" ? plotlyDarkTemplate : {}, } + const xValues = rankPlotInfo.xvalues + const yValues = rankPlotInfo.yvalues + const plotData: Partial[] = [ { type: "scatter", From a801b89c5f281a53cc7c850a4457ea671db77019 Mon Sep 17 00:00:00 2001 From: keisuke-umezawa Date: Thu, 23 Nov 2023 18:57:12 +0900 Subject: [PATCH 113/176] Add codecov badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index badea560..13022125 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ ![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square) [![PyPI - Downloads](https://img.shields.io/pypi/dm/optuna-dashboard)](https://pypistats.org/packages/optuna-dashboard) [![Read the Docs](https://readthedocs.org/projects/optuna-dashboard/badge/?version=latest)](https://optuna-dashboard.readthedocs.io/en/latest/?badge=latest) +[![Codecov](https://codecov.io/gh/optuna/optuna-dashboard/branch/main/graph/badge.svg)](https://codecov.io/gh/optuna/optuna-dashboard) Real-time dashboard for [Optuna](https://github.com/optuna/optuna). From bf7c930aed187ecc43f6776f4ac9623a7bee3ba9 Mon Sep 17 00:00:00 2001 From: hrntsm Date: Sun, 26 Nov 2023 17:43:05 +0900 Subject: [PATCH 114/176] Add dtype to randint --- optuna_dashboard/preferential/samplers/gp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/preferential/samplers/gp.py b/optuna_dashboard/preferential/samplers/gp.py index 3cd98061..590c5570 100644 --- a/optuna_dashboard/preferential/samplers/gp.py +++ b/optuna_dashboard/preferential/samplers/gp.py @@ -317,7 +317,7 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler): self._rng = np.random.RandomState(seed) self.independent_sampler = independent_sampler or optuna.samplers.RandomSampler( - seed=self._rng.randint(2**32) + seed=self._rng.randint(2**32, dtype=np.int64) ) self._search_space = optuna.search_space.IntersectionSearchSpace() @@ -355,7 +355,7 @@ class PreferentialGPSampler(optuna.samplers.BaseSampler): ) pref_ids = torch.tensor([[ids[b], ids[w]] for b, w in preferences], dtype=torch.int32) with torch.random.fork_rng(): - torch.manual_seed(self._rng.randint(2**32)) + torch.manual_seed(self._rng.randint(2**32, dtype=np.int64)) self._gp = self._gp or _PreferentialGP( kernel=self.kernel From ba23617127b12fe228144aa86666c2bb723a7a37 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Mon, 27 Nov 2023 14:57:01 +0900 Subject: [PATCH 115/176] Update test_backend.py --- python_tests/artifact/test_backend.py | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/python_tests/artifact/test_backend.py b/python_tests/artifact/test_backend.py index 22544e6f..bdd6c00d 100644 --- a/python_tests/artifact/test_backend.py +++ b/python_tests/artifact/test_backend.py @@ -1,9 +1,17 @@ +import tempfile +from unittest import TestCase from unittest.mock import MagicMock +import optuna +from optuna.artifacts import FileSystemArtifactStore +from optuna.artifacts import upload_artifact from optuna.storages import BaseStorage +from optuna_dashboard._app import create_app from optuna_dashboard.artifact import _backend import pytest +from ..wsgi_client import send_request + def test_get_artifact_path() -> None: study = MagicMock(_study_id=0) @@ -80,3 +88,49 @@ def test_list_trial_artifacts(init_storage_with_artifact_meta: MagicMock) -> Non {"artifact_id": "id1", "filename": "bar.txt"}, {"artifact_id": "id2", "filename": "baz.txt"}, ] + + +class TestProxyStudyArtifact(TestCase): + def setUp(self) -> None: + self.storage = optuna.storages.InMemoryStorage() + self.study = optuna.create_study(storage=self.storage) + + def test_artifact_store_none(self) -> None: + app = create_app(self.storage) + status, _, body = send_request( + app, + "/artifacts/0/0", + "GET", + content_type="application/json", + ) + self.assertEqual(status, 400) + self.assertEqual(body, b"Cannot access to the artifacts.") + + def test_artifact_not_found(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + app = create_app(self.storage, artifact_store) + status, _, body = send_request( + app, + f"/artifacts/{self.study._study_id}/abc123", + "GET", + content_type="application/json", + ) + self.assertEqual(status, 404) + self.assertEqual(body, b"Not Found") + + def test_successful_artifact_retrieval(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + with tempfile.NamedTemporaryFile() as f: + f.write(b"dummy_content") + f.flush() + artifact_id = upload_artifact(self.study, f.name, artifact_store=artifact_store) + app = create_app(self.storage, artifact_store) + status, _, _ = send_request( + app, + f"/artifacts/{self.study._study_id}/{artifact_id}", + "GET", + content_type="application/json", + ) + self.assertEqual(status, 200) From f29a6e9c0c57ed6c52e113dbc04d6f3aac3efff5 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Mon, 27 Nov 2023 07:30:37 +0100 Subject: [PATCH 116/176] Address the comments by c-bata --- python_tests/test_api.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/python_tests/test_api.py b/python_tests/test_api.py index 0250ad66..601362a1 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -302,21 +302,24 @@ class APITestCase(TestCase): request_body: dict[str, int | str] = {"body": "Test note.", "version": 1} status, study = self._test_save_trial_note(request_body) self.assertEqual(status, 204) - assert study.system_attrs == { + expected_system_attrs = { note_ver_key(0): request_body["version"], f"{note_str_key_prefix(0)}{0}": request_body["body"], } + for k, v in expected_system_attrs.items(): + assert k in study.system_attrs + assert study.system_attrs[k] == v def test_save_trial_note_with_wrong_version(self) -> None: request_body: dict[str, int | str] = {"body": "Test note.", "version": 0} status, study = self._test_save_trial_note(request_body) self.assertEqual(status, 409) - assert study.system_attrs == {} + assert note_ver_key(0) not in study.system_attrs def test_save_trial_note_empty(self) -> None: status, study = self._test_save_trial_note(request_body={}) self.assertEqual(status, 400) - assert study.system_attrs == {} + assert note_ver_key(0) not in study.system_attrs @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") def test_skip_trial(self) -> None: From 6838dd27da9d3fffe0b944952f900b60e832f879 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Mon, 27 Nov 2023 07:31:50 +0100 Subject: [PATCH 117/176] Rename util method in test_save_trial_note --- python_tests/test_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python_tests/test_api.py b/python_tests/test_api.py index 601362a1..01f4d800 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -263,7 +263,7 @@ class APITestCase(TestCase): self.assertEqual(status, 400) assert study.trials[0].user_attrs == {} - def _test_save_trial_note( + def _save_trial_note( self, request_body: dict[str, int | str] ) -> tuple[int, optuna.Study]: study = optuna.create_study() @@ -300,7 +300,7 @@ class APITestCase(TestCase): def test_save_trial_note(self) -> None: request_body: dict[str, int | str] = {"body": "Test note.", "version": 1} - status, study = self._test_save_trial_note(request_body) + status, study = self._save_trial_note(request_body) self.assertEqual(status, 204) expected_system_attrs = { note_ver_key(0): request_body["version"], @@ -312,12 +312,12 @@ class APITestCase(TestCase): def test_save_trial_note_with_wrong_version(self) -> None: request_body: dict[str, int | str] = {"body": "Test note.", "version": 0} - status, study = self._test_save_trial_note(request_body) + status, study = self._save_trial_note(request_body) self.assertEqual(status, 409) assert note_ver_key(0) not in study.system_attrs def test_save_trial_note_empty(self) -> None: - status, study = self._test_save_trial_note(request_body={}) + status, study = self._save_trial_note(request_body={}) self.assertEqual(status, 400) assert note_ver_key(0) not in study.system_attrs From ca98208cca3caebbf52b04c04fefa60efa2520eb Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Mon, 27 Nov 2023 07:33:38 +0100 Subject: [PATCH 118/176] Replace assertEqual with == because we use pytest as a runner --- python_tests/test_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python_tests/test_api.py b/python_tests/test_api.py index 01f4d800..3c5981ea 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -291,7 +291,7 @@ class APITestCase(TestCase): content_type="application/json", body=json.dumps(request_body), ) - self.assertEqual(status, 204) + assert status == 204 # Check if the version 1 is deleted. assert study.system_attrs == { note_ver_key(0): request_body["version"], @@ -301,7 +301,7 @@ class APITestCase(TestCase): def test_save_trial_note(self) -> None: request_body: dict[str, int | str] = {"body": "Test note.", "version": 1} status, study = self._save_trial_note(request_body) - self.assertEqual(status, 204) + assert status == 204 expected_system_attrs = { note_ver_key(0): request_body["version"], f"{note_str_key_prefix(0)}{0}": request_body["body"], @@ -313,12 +313,12 @@ class APITestCase(TestCase): def test_save_trial_note_with_wrong_version(self) -> None: request_body: dict[str, int | str] = {"body": "Test note.", "version": 0} status, study = self._save_trial_note(request_body) - self.assertEqual(status, 409) + assert status == 409 assert note_ver_key(0) not in study.system_attrs def test_save_trial_note_empty(self) -> None: status, study = self._save_trial_note(request_body={}) - self.assertEqual(status, 400) + assert status == 400 assert note_ver_key(0) not in study.system_attrs @pytest.mark.skipif(sys.version_info < (3, 8), reason="BoTorch dropped Python3.7 support") From 616832c98b3127329ecaa29a289b10420910648a Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Mon, 27 Nov 2023 07:35:12 +0100 Subject: [PATCH 119/176] Apply black to test_api --- python_tests/test_api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python_tests/test_api.py b/python_tests/test_api.py index 3c5981ea..2647ed1f 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -263,9 +263,7 @@ class APITestCase(TestCase): self.assertEqual(status, 400) assert study.trials[0].user_attrs == {} - def _save_trial_note( - self, request_body: dict[str, int | str] - ) -> tuple[int, optuna.Study]: + def _save_trial_note(self, request_body: dict[str, int | str]) -> tuple[int, optuna.Study]: study = optuna.create_study() trial = study.ask() app = create_app(study._storage) From 09c95197d8c6c9a5c4b25dc41bd4f2e85aa749f7 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Mon, 27 Nov 2023 07:41:37 +0100 Subject: [PATCH 120/176] Make test_save_trial_note_overwrite more robust --- python_tests/test_api.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/python_tests/test_api.py b/python_tests/test_api.py index 2647ed1f..e7210710 100644 --- a/python_tests/test_api.py +++ b/python_tests/test_api.py @@ -280,21 +280,28 @@ class APITestCase(TestCase): study = optuna.create_study() trial = study.ask() app = create_app(study._storage) + + def _get_request_body(note_version: int) -> dict[str, str | int]: + return {"body": f"Test note ver. {note_version}.", "version": note_version} + for ver in range(1, 3): - request_body = {"body": f"Test note ver. {ver}.", "version": ver} status, _, _ = send_request( app, f"/api/studies/{study._study_id}/{trial._trial_id}/note", "PUT", content_type="application/json", - body=json.dumps(request_body), + body=json.dumps(_get_request_body(note_version=ver)), ) assert status == 204 # Check if the version 1 is deleted. - assert study.system_attrs == { - note_ver_key(0): request_body["version"], - f"{note_str_key_prefix(0)}{0}": request_body["body"], + expected_request_body = _get_request_body(note_version=2) + expected_system_attrs = { + note_ver_key(trial_id=0): expected_request_body["version"], + f"{note_str_key_prefix(trial_id=0)}{0}": expected_request_body["body"], } + for k, v in expected_system_attrs.items(): + assert k in study.system_attrs + assert study.system_attrs[k] == v def test_save_trial_note(self) -> None: request_body: dict[str, int | str] = {"body": "Test note.", "version": 1} From 4b439a69970c770809ef0e320ac5ea79f3c99685 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Mon, 27 Nov 2023 16:29:00 +0900 Subject: [PATCH 121/176] Update python_tests/artifact/test_backend.py Co-authored-by: c-bata --- python_tests/artifact/test_backend.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python_tests/artifact/test_backend.py b/python_tests/artifact/test_backend.py index bdd6c00d..d7df189a 100644 --- a/python_tests/artifact/test_backend.py +++ b/python_tests/artifact/test_backend.py @@ -104,7 +104,6 @@ class TestProxyStudyArtifact(TestCase): content_type="application/json", ) self.assertEqual(status, 400) - self.assertEqual(body, b"Cannot access to the artifacts.") def test_artifact_not_found(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: From c89aac71d63f00aca798e25eb605b7e834ec5fc6 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Mon, 27 Nov 2023 16:29:08 +0900 Subject: [PATCH 122/176] Update python_tests/artifact/test_backend.py Co-authored-by: c-bata --- python_tests/artifact/test_backend.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python_tests/artifact/test_backend.py b/python_tests/artifact/test_backend.py index d7df189a..07ea34d7 100644 --- a/python_tests/artifact/test_backend.py +++ b/python_tests/artifact/test_backend.py @@ -116,7 +116,6 @@ class TestProxyStudyArtifact(TestCase): content_type="application/json", ) self.assertEqual(status, 404) - self.assertEqual(body, b"Not Found") def test_successful_artifact_retrieval(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: From e3cad622f7dc7a96dc983cfef6f03b480a999e41 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Mon, 27 Nov 2023 16:29:17 +0900 Subject: [PATCH 123/176] Update python_tests/artifact/test_backend.py Co-authored-by: c-bata --- python_tests/artifact/test_backend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python_tests/artifact/test_backend.py b/python_tests/artifact/test_backend.py index 07ea34d7..143e86b4 100644 --- a/python_tests/artifact/test_backend.py +++ b/python_tests/artifact/test_backend.py @@ -125,10 +125,11 @@ class TestProxyStudyArtifact(TestCase): f.flush() artifact_id = upload_artifact(self.study, f.name, artifact_store=artifact_store) app = create_app(self.storage, artifact_store) - status, _, _ = send_request( + status, _, body = send_request( app, f"/artifacts/{self.study._study_id}/{artifact_id}", "GET", content_type="application/json", ) self.assertEqual(status, 200) + self.assertEqual(body, b"dummy_content") From ff210f50d64ab864bca0cf31f6fad854cd1be1f0 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Mon, 27 Nov 2023 16:30:28 +0900 Subject: [PATCH 124/176] Update test_backend.py --- python_tests/artifact/test_backend.py | 73 ++++++++++++++------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/python_tests/artifact/test_backend.py b/python_tests/artifact/test_backend.py index 143e86b4..00e55d51 100644 --- a/python_tests/artifact/test_backend.py +++ b/python_tests/artifact/test_backend.py @@ -1,5 +1,4 @@ import tempfile -from unittest import TestCase from unittest.mock import MagicMock import optuna @@ -90,46 +89,48 @@ def test_list_trial_artifacts(init_storage_with_artifact_meta: MagicMock) -> Non ] -class TestProxyStudyArtifact(TestCase): - def setUp(self) -> None: - self.storage = optuna.storages.InMemoryStorage() - self.study = optuna.create_study(storage=self.storage) +def test_artifact_store_none() -> None: + storage = optuna.storages.InMemoryStorage() + app = create_app(storage) + status, _, body = send_request( + app, + "/artifacts/0/0", + "GET", + content_type="application/json", + ) + assert status == 400 - def test_artifact_store_none(self) -> None: - app = create_app(self.storage) + +def test_artifact_not_found() -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + app = create_app(storage, artifact_store) status, _, body = send_request( app, - "/artifacts/0/0", + f"/artifacts/{study._study_id}/abc123", "GET", content_type="application/json", ) - self.assertEqual(status, 400) + assert status == 404 - def test_artifact_not_found(self) -> None: - with tempfile.TemporaryDirectory() as tmpdir: - artifact_store = FileSystemArtifactStore(tmpdir) - app = create_app(self.storage, artifact_store) - status, _, body = send_request( - app, - f"/artifacts/{self.study._study_id}/abc123", - "GET", - content_type="application/json", - ) - self.assertEqual(status, 404) - def test_successful_artifact_retrieval(self) -> None: - with tempfile.TemporaryDirectory() as tmpdir: - artifact_store = FileSystemArtifactStore(tmpdir) - with tempfile.NamedTemporaryFile() as f: - f.write(b"dummy_content") - f.flush() - artifact_id = upload_artifact(self.study, f.name, artifact_store=artifact_store) - app = create_app(self.storage, artifact_store) - status, _, body = send_request( - app, - f"/artifacts/{self.study._study_id}/{artifact_id}", - "GET", - content_type="application/json", - ) - self.assertEqual(status, 200) - self.assertEqual(body, b"dummy_content") +def test_successful_artifact_retrieval() -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + with tempfile.NamedTemporaryFile() as f: + f.write(b"dummy_content") + f.flush() + artifact_id = upload_artifact(study, f.name, artifact_store=artifact_store) + app = create_app(storage, artifact_store) + status, _, body = send_request( + app, + f"/artifacts/{study._study_id}/{artifact_id}", + "GET", + content_type="application/json", + ) + assert status == 200 + assert body == b"dummy_content" From 6a55acb662bce6c0e3d43d78f5bf341d91bf1c7d Mon Sep 17 00:00:00 2001 From: c-bata Date: Tue, 28 Nov 2023 14:20:51 +0900 Subject: [PATCH 125/176] Apply suggestions from code review Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- optuna_dashboard/ts/components/DataGrid.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/optuna_dashboard/ts/components/DataGrid.tsx b/optuna_dashboard/ts/components/DataGrid.tsx index faa7e75c..ab5b22e4 100644 --- a/optuna_dashboard/ts/components/DataGrid.tsx +++ b/optuna_dashboard/ts/components/DataGrid.tsx @@ -99,11 +99,11 @@ function DataGrid(props: { return true } const toCellValue = columns[f.columnIdx].toCellValue - const value = + const cellValue = toCellValue !== undefined ? toCellValue(rowIdx) : row[columns[f.columnIdx].field] - return f.values.some((v) => v === value) + return f.values.some((v) => v === cellValue) }) }) @@ -266,17 +266,17 @@ function DataGridHeaderColumn(props: { { - const values = + const newTickedValues = filter === null - ? filterChoices.filter((v) => v !== choice) + ? filterChoices.filter((v) => v !== choice) // By default, every choice is ticked, so the chosen option will be unticked. : filter.values.some((v) => v === choice) ? filter.values.filter((v) => v !== choice) : [...filter.values, choice] - onFilterChange(values) + onFilterChange(newTickedValues) }} > - {!filter || !filter.values.every((v) => v !== choice) ? ( + {!filter || filter.values.some((v) => v === choice) ? ( ) : ( From 18ed414c9af62c7c535a957b34006c7d5950beae Mon Sep 17 00:00:00 2001 From: c-bata Date: Tue, 28 Nov 2023 14:21:43 +0900 Subject: [PATCH 126/176] Fix lint errors --- optuna_dashboard/ts/components/DataGrid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/ts/components/DataGrid.tsx b/optuna_dashboard/ts/components/DataGrid.tsx index ab5b22e4..4c24426f 100644 --- a/optuna_dashboard/ts/components/DataGrid.tsx +++ b/optuna_dashboard/ts/components/DataGrid.tsx @@ -268,7 +268,7 @@ function DataGridHeaderColumn(props: { onClick={() => { const newTickedValues = filter === null - ? filterChoices.filter((v) => v !== choice) // By default, every choice is ticked, so the chosen option will be unticked. + ? filterChoices.filter((v) => v !== choice) // By default, every choice is ticked, so the chosen option will be unticked. : filter.values.some((v) => v === choice) ? filter.values.filter((v) => v !== choice) : [...filter.values, choice] From 265dfb4c95afe04f4d30c9df0044c584aa48a158 Mon Sep 17 00:00:00 2001 From: c-bata Date: Tue, 28 Nov 2023 14:57:01 +0900 Subject: [PATCH 127/176] Update trial artifacts after uploaded --- optuna_dashboard/artifact/_backend.py | 1 + 1 file changed, 1 insertion(+) diff --git a/optuna_dashboard/artifact/_backend.py b/optuna_dashboard/artifact/_backend.py index 9720124b..81cd766d 100644 --- a/optuna_dashboard/artifact/_backend.py +++ b/optuna_dashboard/artifact/_backend.py @@ -139,6 +139,7 @@ def register_artifact_route( storage.set_trial_system_attr(trial_id, attr_key, json.dumps(artifact)) response.status = 201 + trial = storage.get_trial(trial_id) # Fetch trial.system_attrs again. return { "artifact_id": artifact_id, "artifacts": list_trial_artifacts(storage.get_study_system_attrs(study_id), trial), From d31ab9a68ddcec2e086538d722a73d334cc930b6 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Tue, 28 Nov 2023 15:32:21 +0900 Subject: [PATCH 128/176] Add tests for proxy_trial_artifact --- python_tests/artifact/test_backend.py | 55 ++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/python_tests/artifact/test_backend.py b/python_tests/artifact/test_backend.py index 00e55d51..14a470e3 100644 --- a/python_tests/artifact/test_backend.py +++ b/python_tests/artifact/test_backend.py @@ -89,19 +89,18 @@ def test_list_trial_artifacts(init_storage_with_artifact_meta: MagicMock) -> Non ] -def test_artifact_store_none() -> None: +def test_study_artifact_store_none() -> None: storage = optuna.storages.InMemoryStorage() app = create_app(storage) status, _, body = send_request( app, "/artifacts/0/0", "GET", - content_type="application/json", ) assert status == 400 -def test_artifact_not_found() -> None: +def test_study_artifact_not_found() -> None: storage = optuna.storages.InMemoryStorage() study = optuna.create_study(storage=storage) with tempfile.TemporaryDirectory() as tmpdir: @@ -111,12 +110,11 @@ def test_artifact_not_found() -> None: app, f"/artifacts/{study._study_id}/abc123", "GET", - content_type="application/json", ) assert status == 404 -def test_successful_artifact_retrieval() -> None: +def test_successful_study_artifact_retrieval() -> None: storage = optuna.storages.InMemoryStorage() study = optuna.create_study(storage=storage) with tempfile.TemporaryDirectory() as tmpdir: @@ -130,7 +128,52 @@ def test_successful_artifact_retrieval() -> None: app, f"/artifacts/{study._study_id}/{artifact_id}", "GET", - content_type="application/json", + ) + assert status == 200 + assert body == b"dummy_content" + + +def test_trial_artifact_store_none() -> None: + storage = optuna.storages.InMemoryStorage() + app = create_app(storage) + status, _, body = send_request( + app, + "/artifacts/0/0/0", + "GET", + ) + assert status == 400 + + +def test_trial_artifact_not_found() -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + trial = study.ask() + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + app = create_app(storage, artifact_store) + status, _, body = send_request( + app, + f"/artifacts/{study._study_id}/{trial._trial_id}/abc123", + "GET", + ) + assert status == 404 + + +def test_successful_trial_artifact_retrieval() -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + trial = study.ask() + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + with tempfile.NamedTemporaryFile() as f: + f.write(b"dummy_content") + f.flush() + artifact_id = upload_artifact(trial, f.name, artifact_store=artifact_store) + app = create_app(storage, artifact_store) + status, _, body = send_request( + app, + f"/artifacts/{study._study_id}/{trial._trial_id}/{artifact_id}", + "GET", ) assert status == 200 assert body == b"dummy_content" From d52939bd8acef13fc4eb4ae5f26d73aa5cdf0792 Mon Sep 17 00:00:00 2001 From: Victoria A <52001888+adjeiv@users.noreply.github.com> Date: Tue, 28 Nov 2023 13:57:15 +0000 Subject: [PATCH 129/176] Transfer to copy, and remove delete API fns --- .ipynb_checkpoints/MD Bug-checkpoint.ipynb | 104 +++++++++++++++++++ .ipynb_checkpoints/Untitled-checkpoint.ipynb | 6 ++ MD Bug.ipynb | 87 ++++++++++++++++ Untitled.ipynb | 85 +++++++++++++++ optuna_dashboard/_app.py | 3 +- optuna_dashboard/_note.py | 49 +++------ python_tests/test_note.py | 52 +--------- 7 files changed, 297 insertions(+), 89 deletions(-) create mode 100644 .ipynb_checkpoints/MD Bug-checkpoint.ipynb create mode 100644 .ipynb_checkpoints/Untitled-checkpoint.ipynb create mode 100644 MD Bug.ipynb create mode 100644 Untitled.ipynb diff --git a/.ipynb_checkpoints/MD Bug-checkpoint.ipynb b/.ipynb_checkpoints/MD Bug-checkpoint.ipynb new file mode 100644 index 00000000..980a307a --- /dev/null +++ b/.ipynb_checkpoints/MD Bug-checkpoint.ipynb @@ -0,0 +1,104 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 5, + "id": "32593824-793e-486e-8494-7f18f73ae5ee", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2023-11-18 12:06:31,357] A new study created in RDB with name: mdbug2\n", + "[W 2023-11-18 12:06:31,454] Trial 0 failed with parameters: {} because of the following error: TypeError(\"tpe_objective_fn() missing 1 required positional argument: 'observations'\").\n", + "Traceback (most recent call last):\n", + " File \"C:\\Users\\victo\\Documents\\OpenSource\\optuna_workspace\\optuna\\optuna\\study\\_optimize.py\", line 200, in _run_trial\n", + " value_or_values = func(trial)\n", + " ^^^^^^^^^^^\n", + "TypeError: tpe_objective_fn() missing 1 required positional argument: 'observations'\n", + "[W 2023-11-18 12:06:31,457] Trial 0 failed with value None.\n" + ] + }, + { + "ename": "TypeError", + "evalue": "tpe_objective_fn() missing 1 required positional argument: 'observations'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[5], line 30\u001b[0m\n\u001b[0;32m 25\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;18m__name__\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m__main__\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 26\u001b[0m study \u001b[38;5;241m=\u001b[39m optuna\u001b[38;5;241m.\u001b[39mcreate_study(\n\u001b[0;32m 27\u001b[0m storage\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msqlite:///db.sqlite3\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;66;03m# Specify the storage URL here.\u001b[39;00m\n\u001b[0;32m 28\u001b[0m study_name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmdbug2\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 29\u001b[0m )\n\u001b[1;32m---> 30\u001b[0m \u001b[43mstudy\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43moptimize\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtpe_objective_fn\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mn_trials\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m7\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[0;32m 31\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mBest value: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstudy\u001b[38;5;241m.\u001b[39mbest_value\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m (params: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstudy\u001b[38;5;241m.\u001b[39mbest_params\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m)\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "File \u001b[1;32m~\\Documents\\OpenSource\\optuna_workspace\\optuna\\optuna\\study\\study.py:451\u001b[0m, in \u001b[0;36mStudy.optimize\u001b[1;34m(self, func, n_trials, timeout, n_jobs, catch, callbacks, gc_after_trial, show_progress_bar)\u001b[0m\n\u001b[0;32m 348\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21moptimize\u001b[39m(\n\u001b[0;32m 349\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m 350\u001b[0m func: ObjectiveFuncType,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 357\u001b[0m show_progress_bar: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[0;32m 358\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m 359\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Optimize an objective function.\u001b[39;00m\n\u001b[0;32m 360\u001b[0m \n\u001b[0;32m 361\u001b[0m \u001b[38;5;124;03m Optimization is done by choosing a suitable set of hyperparameter values from a given\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 449\u001b[0m \u001b[38;5;124;03m If nested invocation of this method occurs.\u001b[39;00m\n\u001b[0;32m 450\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m--> 451\u001b[0m \u001b[43m_optimize\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 452\u001b[0m \u001b[43m \u001b[49m\u001b[43mstudy\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 453\u001b[0m \u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 454\u001b[0m \u001b[43m \u001b[49m\u001b[43mn_trials\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mn_trials\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 455\u001b[0m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtimeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 456\u001b[0m \u001b[43m \u001b[49m\u001b[43mn_jobs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mn_jobs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 457\u001b[0m \u001b[43m \u001b[49m\u001b[43mcatch\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mtuple\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mcatch\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43misinstance\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mcatch\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mIterable\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[43mcatch\u001b[49m\u001b[43m,\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 458\u001b[0m \u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcallbacks\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 459\u001b[0m \u001b[43m \u001b[49m\u001b[43mgc_after_trial\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mgc_after_trial\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 460\u001b[0m \u001b[43m \u001b[49m\u001b[43mshow_progress_bar\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mshow_progress_bar\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 461\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32m~\\Documents\\OpenSource\\optuna_workspace\\optuna\\optuna\\study\\_optimize.py:66\u001b[0m, in \u001b[0;36m_optimize\u001b[1;34m(study, func, n_trials, timeout, n_jobs, catch, callbacks, gc_after_trial, show_progress_bar)\u001b[0m\n\u001b[0;32m 64\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 65\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n_jobs \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[1;32m---> 66\u001b[0m \u001b[43m_optimize_sequential\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 67\u001b[0m \u001b[43m \u001b[49m\u001b[43mstudy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 68\u001b[0m \u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 69\u001b[0m \u001b[43m \u001b[49m\u001b[43mn_trials\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 70\u001b[0m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 71\u001b[0m \u001b[43m \u001b[49m\u001b[43mcatch\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 72\u001b[0m \u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 73\u001b[0m \u001b[43m \u001b[49m\u001b[43mgc_after_trial\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 74\u001b[0m \u001b[43m \u001b[49m\u001b[43mreseed_sampler_rng\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m 75\u001b[0m \u001b[43m \u001b[49m\u001b[43mtime_start\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m 76\u001b[0m \u001b[43m \u001b[49m\u001b[43mprogress_bar\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mprogress_bar\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 77\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 78\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 79\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n_jobs \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m:\n", + "File \u001b[1;32m~\\Documents\\OpenSource\\optuna_workspace\\optuna\\optuna\\study\\_optimize.py:163\u001b[0m, in \u001b[0;36m_optimize_sequential\u001b[1;34m(study, func, n_trials, timeout, catch, callbacks, gc_after_trial, reseed_sampler_rng, time_start, progress_bar)\u001b[0m\n\u001b[0;32m 160\u001b[0m \u001b[38;5;28;01mbreak\u001b[39;00m\n\u001b[0;32m 162\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 163\u001b[0m frozen_trial \u001b[38;5;241m=\u001b[39m \u001b[43m_run_trial\u001b[49m\u001b[43m(\u001b[49m\u001b[43mstudy\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcatch\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 164\u001b[0m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[0;32m 165\u001b[0m \u001b[38;5;66;03m# The following line mitigates memory problems that can be occurred in some\u001b[39;00m\n\u001b[0;32m 166\u001b[0m \u001b[38;5;66;03m# environments (e.g., services that use computing containers such as GitHub Actions).\u001b[39;00m\n\u001b[0;32m 167\u001b[0m \u001b[38;5;66;03m# Please refer to the following PR for further details:\u001b[39;00m\n\u001b[0;32m 168\u001b[0m \u001b[38;5;66;03m# https://github.com/optuna/optuna/pull/325.\u001b[39;00m\n\u001b[0;32m 169\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m gc_after_trial:\n", + "File \u001b[1;32m~\\Documents\\OpenSource\\optuna_workspace\\optuna\\optuna\\study\\_optimize.py:251\u001b[0m, in \u001b[0;36m_run_trial\u001b[1;34m(study, func, catch)\u001b[0m\n\u001b[0;32m 244\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;28;01mFalse\u001b[39;00m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mShould not reach.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 246\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m (\n\u001b[0;32m 247\u001b[0m frozen_trial\u001b[38;5;241m.\u001b[39mstate \u001b[38;5;241m==\u001b[39m TrialState\u001b[38;5;241m.\u001b[39mFAIL\n\u001b[0;32m 248\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m func_err \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 249\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(func_err, catch)\n\u001b[0;32m 250\u001b[0m ):\n\u001b[1;32m--> 251\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m func_err\n\u001b[0;32m 252\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m frozen_trial\n", + "File \u001b[1;32m~\\Documents\\OpenSource\\optuna_workspace\\optuna\\optuna\\study\\_optimize.py:200\u001b[0m, in \u001b[0;36m_run_trial\u001b[1;34m(study, func, catch)\u001b[0m\n\u001b[0;32m 198\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m get_heartbeat_thread(trial\u001b[38;5;241m.\u001b[39m_trial_id, study\u001b[38;5;241m.\u001b[39m_storage):\n\u001b[0;32m 199\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 200\u001b[0m value_or_values \u001b[38;5;241m=\u001b[39m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtrial\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 201\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m exceptions\u001b[38;5;241m.\u001b[39mTrialPruned \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m 202\u001b[0m \u001b[38;5;66;03m# TODO(mamu): Handle multi-objective cases.\u001b[39;00m\n\u001b[0;32m 203\u001b[0m state \u001b[38;5;241m=\u001b[39m TrialState\u001b[38;5;241m.\u001b[39mPRUNED\n", + "\u001b[1;31mTypeError\u001b[0m: tpe_objective_fn() missing 1 required positional argument: 'observations'" + ] + } + ], + "source": [ + "import optuna\n", + "import numpy as np\n", + "\n", + "def cost_function(x, y):\n", + " return np.rand(0, 1)\n", + "\n", + "def tpe_objective_fn(trial):\n", + " num_steps = 5\n", + " num_heat_sources = 7\n", + " lb = [0, 1, 2, 3, 4, 5, 6]\n", + " ub = [4, 5, 6, 7, 8, 9, 10]\n", + " chosen_parameters = []\n", + " for step in range(num_steps):\n", + " for idx in range(num_heat_sources):\n", + " chosen_parameters.append(\n", + " trial.suggest_float(f\"tech_param_{step}_{idx}\", lb[idx], ub[idx])\n", + " )\n", + "\n", + " cost = cost_function(\n", + " chosen_parameters,\n", + " num_steps,\n", + " )\n", + " return cost\n", + "\n", + "if __name__ == \"__main__\":\n", + " study = optuna.create_study(\n", + " storage=\"sqlite:///db.sqlite3\", # Specify the storage URL here.\n", + " study_name=\"mdbug2\"\n", + " )\n", + " study.optimize(tpe_objective_fn, n_trials=7)\n", + " print(f\"Best value: {study.best_value} (params: {study.best_params})\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c66fec2-3e46-46c4-bc55-e784d237a3e5", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.ipynb_checkpoints/Untitled-checkpoint.ipynb b/.ipynb_checkpoints/Untitled-checkpoint.ipynb new file mode 100644 index 00000000..363fcab7 --- /dev/null +++ b/.ipynb_checkpoints/Untitled-checkpoint.ipynb @@ -0,0 +1,6 @@ +{ + "cells": [], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/MD Bug.ipynb b/MD Bug.ipynb new file mode 100644 index 00000000..75b0ea27 --- /dev/null +++ b/MD Bug.ipynb @@ -0,0 +1,87 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 10, + "id": "32593824-793e-486e-8494-7f18f73ae5ee", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[I 2023-11-18 12:13:05,829] A new study created in RDB with name: mdbugb\n", + "[I 2023-11-18 12:13:06,187] Trial 0 finished with value: 0.9 and parameters: {'tech_param_0_0': 0.9863311667179313, 'tech_param_0_1': 2.9722067965404126, 'tech_param_0_2': 5.479907499743063, 'tech_param_0_3': 6.193147335867399, 'tech_param_0_4': 6.44329845141624, 'tech_param_0_5': 6.957704292533055, 'tech_param_0_6': 6.028116976338117, 'tech_param_1_0': 0.8528770906850203, 'tech_param_1_1': 2.8845780424110843, 'tech_param_1_2': 2.205086423835636, 'tech_param_1_3': 5.8965466201793735, 'tech_param_1_4': 6.765916728183967, 'tech_param_1_5': 5.216406754175934, 'tech_param_1_6': 9.921684209548362, 'tech_param_2_0': 3.9278254745655903, 'tech_param_2_1': 2.8605537473963465, 'tech_param_2_2': 4.466559550814772, 'tech_param_2_3': 4.838464946220083, 'tech_param_2_4': 4.026691756550308, 'tech_param_2_5': 5.3788556566702805, 'tech_param_2_6': 6.675770800145582, 'tech_param_3_0': 3.031562395355724, 'tech_param_3_1': 3.7349707362119777, 'tech_param_3_2': 5.473994937262473, 'tech_param_3_3': 6.881019240236522, 'tech_param_3_4': 6.120001351513482, 'tech_param_3_5': 6.036086717431755, 'tech_param_3_6': 9.160127979158023, 'tech_param_4_0': 2.758775673013467, 'tech_param_4_1': 1.0680707015794786, 'tech_param_4_2': 3.3292082013802666, 'tech_param_4_3': 6.687649735544305, 'tech_param_4_4': 7.443681016669299, 'tech_param_4_5': 7.162694801047951, 'tech_param_4_6': 9.999288376422493}. Best is trial 0 with value: 0.9.\n", + "[I 2023-11-18 12:13:06,501] Trial 1 finished with value: 0.9 and parameters: {'tech_param_0_0': 1.7140076646332538, 'tech_param_0_1': 3.2316252813476325, 'tech_param_0_2': 4.306343376567941, 'tech_param_0_3': 4.61192706012931, 'tech_param_0_4': 5.988119724240175, 'tech_param_0_5': 6.9327357154708285, 'tech_param_0_6': 6.53997726319049, 'tech_param_1_0': 0.3905783000188636, 'tech_param_1_1': 2.76165385255339, 'tech_param_1_2': 2.6085969410537855, 'tech_param_1_3': 4.749816941187031, 'tech_param_1_4': 4.846918571308436, 'tech_param_1_5': 6.254401319703383, 'tech_param_1_6': 7.8373063069546935, 'tech_param_2_0': 2.91103305557099, 'tech_param_2_1': 4.471679319725785, 'tech_param_2_2': 3.5984067288829946, 'tech_param_2_3': 3.470290937133842, 'tech_param_2_4': 7.799315424111551, 'tech_param_2_5': 7.826822170149293, 'tech_param_2_6': 8.849116163752687, 'tech_param_3_0': 3.5380619781168816, 'tech_param_3_1': 3.653451480021751, 'tech_param_3_2': 3.47434126199421, 'tech_param_3_3': 4.443348897470383, 'tech_param_3_4': 7.045847902681517, 'tech_param_3_5': 6.2594143301415475, 'tech_param_3_6': 7.979774488188764, 'tech_param_4_0': 1.0325150795599196, 'tech_param_4_1': 3.4448235367545186, 'tech_param_4_2': 3.3149959756969167, 'tech_param_4_3': 4.819419990796481, 'tech_param_4_4': 7.240391392837244, 'tech_param_4_5': 6.412867172919929, 'tech_param_4_6': 7.784828203240426}. Best is trial 0 with value: 0.9.\n", + "[I 2023-11-18 12:13:06,830] Trial 2 finished with value: 0.9 and parameters: {'tech_param_0_0': 3.7285153030023213, 'tech_param_0_1': 2.967995773245595, 'tech_param_0_2': 2.1806768067032025, 'tech_param_0_3': 4.166177958423523, 'tech_param_0_4': 7.340303214415031, 'tech_param_0_5': 8.466190260210265, 'tech_param_0_6': 8.838603377394787, 'tech_param_1_0': 1.7630336762562222, 'tech_param_1_1': 1.1619500439780515, 'tech_param_1_2': 3.4038384919507747, 'tech_param_1_3': 6.650347896582192, 'tech_param_1_4': 7.7764728822600055, 'tech_param_1_5': 8.639229218462436, 'tech_param_1_6': 7.113069116576301, 'tech_param_2_0': 3.9931082904645394, 'tech_param_2_1': 3.1473900771322443, 'tech_param_2_2': 4.8958785737518475, 'tech_param_2_3': 4.135490374890335, 'tech_param_2_4': 4.642792333607787, 'tech_param_2_5': 6.804997337959285, 'tech_param_2_6': 7.093905518212265, 'tech_param_3_0': 3.7794491588384793, 'tech_param_3_1': 2.9200030292104078, 'tech_param_3_2': 2.193269350123763, 'tech_param_3_3': 4.4228612589114915, 'tech_param_3_4': 6.768417418097517, 'tech_param_3_5': 7.833147977002255, 'tech_param_3_6': 6.043265945471216, 'tech_param_4_0': 1.239068042928749, 'tech_param_4_1': 3.0223635344959376, 'tech_param_4_2': 4.542741924432429, 'tech_param_4_3': 5.527626147325485, 'tech_param_4_4': 6.41989159985463, 'tech_param_4_5': 5.957849401921937, 'tech_param_4_6': 8.220767432559985}. Best is trial 0 with value: 0.9.\n", + "[I 2023-11-18 12:13:07,175] Trial 3 finished with value: 0.9 and parameters: {'tech_param_0_0': 2.3406080881578437, 'tech_param_0_1': 2.20431580814314, 'tech_param_0_2': 2.315142042718149, 'tech_param_0_3': 3.153313622905333, 'tech_param_0_4': 6.434868333320647, 'tech_param_0_5': 8.010675478290011, 'tech_param_0_6': 9.795451780231673, 'tech_param_1_0': 3.893698909905487, 'tech_param_1_1': 3.448571655312496, 'tech_param_1_2': 2.7450589731117088, 'tech_param_1_3': 5.2194903883501, 'tech_param_1_4': 7.236541468886768, 'tech_param_1_5': 6.823921860165192, 'tech_param_1_6': 9.616564141906174, 'tech_param_2_0': 0.39123634971167975, 'tech_param_2_1': 2.39626451267548, 'tech_param_2_2': 3.0034883416963494, 'tech_param_2_3': 6.376136926494553, 'tech_param_2_4': 7.218911507033649, 'tech_param_2_5': 6.750947297023547, 'tech_param_2_6': 8.736332931103936, 'tech_param_3_0': 0.6196213625340548, 'tech_param_3_1': 3.387324252791688, 'tech_param_3_2': 4.183149086442488, 'tech_param_3_3': 5.183411467900889, 'tech_param_3_4': 6.267654527841776, 'tech_param_3_5': 5.549621056761763, 'tech_param_3_6': 7.672308955901496, 'tech_param_4_0': 1.246623944346387, 'tech_param_4_1': 4.456831294515673, 'tech_param_4_2': 2.0724147884920074, 'tech_param_4_3': 5.379419267307556, 'tech_param_4_4': 7.112725631815934, 'tech_param_4_5': 8.87353147211932, 'tech_param_4_6': 7.693639964574908}. Best is trial 0 with value: 0.9.\n", + "[I 2023-11-18 12:13:07,487] Trial 4 finished with value: 0.9 and parameters: {'tech_param_0_0': 3.1068581046485, 'tech_param_0_1': 2.9287122006776443, 'tech_param_0_2': 5.4057937949623085, 'tech_param_0_3': 3.157484488408773, 'tech_param_0_4': 4.690948899704123, 'tech_param_0_5': 8.145312132270456, 'tech_param_0_6': 6.089344894219481, 'tech_param_1_0': 2.9926885191436527, 'tech_param_1_1': 3.176774691423508, 'tech_param_1_2': 2.7721710618333737, 'tech_param_1_3': 4.67762193566592, 'tech_param_1_4': 4.012363168044638, 'tech_param_1_5': 5.659401760473942, 'tech_param_1_6': 8.830934616907934, 'tech_param_2_0': 1.661839241732621, 'tech_param_2_1': 2.7405670060757923, 'tech_param_2_2': 2.100697848478035, 'tech_param_2_3': 6.253340195464311, 'tech_param_2_4': 6.976724037705612, 'tech_param_2_5': 5.510598362317128, 'tech_param_2_6': 7.522477241180887, 'tech_param_3_0': 2.4453135979613685, 'tech_param_3_1': 1.695095408763359, 'tech_param_3_2': 3.102101960127642, 'tech_param_3_3': 4.087680321929806, 'tech_param_3_4': 6.130088543099206, 'tech_param_3_5': 7.350026673357341, 'tech_param_3_6': 8.078588257404482, 'tech_param_4_0': 3.4813120281559478, 'tech_param_4_1': 3.6566237591430295, 'tech_param_4_2': 4.615726645672355, 'tech_param_4_3': 3.5688464780387226, 'tech_param_4_4': 4.212091370332146, 'tech_param_4_5': 6.790648339845854, 'tech_param_4_6': 9.793540661498884}. Best is trial 0 with value: 0.9.\n", + "[I 2023-11-18 12:13:07,804] Trial 5 finished with value: 0.9 and parameters: {'tech_param_0_0': 3.7955449152390734, 'tech_param_0_1': 3.5879051604064425, 'tech_param_0_2': 5.047936105941167, 'tech_param_0_3': 6.409387696471882, 'tech_param_0_4': 6.139796193716612, 'tech_param_0_5': 5.818035000269156, 'tech_param_0_6': 6.77671788266593, 'tech_param_1_0': 3.538002063564025, 'tech_param_1_1': 4.653916400756065, 'tech_param_1_2': 5.139067082232518, 'tech_param_1_3': 5.258612796259207, 'tech_param_1_4': 5.762995511794475, 'tech_param_1_5': 5.914412902115752, 'tech_param_1_6': 8.821608430328544, 'tech_param_2_0': 2.8391352600362043, 'tech_param_2_1': 1.3147834382265242, 'tech_param_2_2': 5.712530324883787, 'tech_param_2_3': 3.9982127746896365, 'tech_param_2_4': 7.59784603286235, 'tech_param_2_5': 8.249304067800995, 'tech_param_2_6': 9.13797124898645, 'tech_param_3_0': 3.4218288637879235, 'tech_param_3_1': 2.8432006830690506, 'tech_param_3_2': 3.705582741959596, 'tech_param_3_3': 4.932782214722957, 'tech_param_3_4': 5.397522930636624, 'tech_param_3_5': 7.665389302007817, 'tech_param_3_6': 8.161440460609116, 'tech_param_4_0': 0.9002808033064547, 'tech_param_4_1': 2.8214804227095938, 'tech_param_4_2': 4.209001287953269, 'tech_param_4_3': 3.4276241669947267, 'tech_param_4_4': 5.65920280643175, 'tech_param_4_5': 7.0296445059400385, 'tech_param_4_6': 6.58394027927913}. Best is trial 0 with value: 0.9.\n", + "[I 2023-11-18 12:13:08,436] Trial 6 finished with value: 0.9 and parameters: {'tech_param_0_0': 2.1661335557317467, 'tech_param_0_1': 2.10482781209877, 'tech_param_0_2': 3.5267055048813605, 'tech_param_0_3': 3.3403923781628193, 'tech_param_0_4': 7.251793183308415, 'tech_param_0_5': 5.419077835893116, 'tech_param_0_6': 6.92337803745288, 'tech_param_1_0': 2.248588320589992, 'tech_param_1_1': 1.5496877038644339, 'tech_param_1_2': 3.034290526391054, 'tech_param_1_3': 5.70430130433426, 'tech_param_1_4': 4.755971043039262, 'tech_param_1_5': 6.032107463009746, 'tech_param_1_6': 6.485937845252712, 'tech_param_2_0': 1.1198667631159256, 'tech_param_2_1': 4.443832638453198, 'tech_param_2_2': 4.029162583096888, 'tech_param_2_3': 4.421249989146505, 'tech_param_2_4': 7.266267320462586, 'tech_param_2_5': 7.272268040989632, 'tech_param_2_6': 9.34608451054512, 'tech_param_3_0': 3.5400627448638406, 'tech_param_3_1': 1.0291771457580832, 'tech_param_3_2': 3.001266350018537, 'tech_param_3_3': 3.83986701735892, 'tech_param_3_4': 4.21380354332598, 'tech_param_3_5': 5.7510339129863555, 'tech_param_3_6': 9.666174111722901, 'tech_param_4_0': 0.25936572770098243, 'tech_param_4_1': 1.0411384740076457, 'tech_param_4_2': 3.6068625395816722, 'tech_param_4_3': 4.778931228821068, 'tech_param_4_4': 5.259093795072825, 'tech_param_4_5': 8.171709511787906, 'tech_param_4_6': 9.580347111957641}. Best is trial 0 with value: 0.9.\n" + ] + } + ], + "source": [ + "import optuna\n", + "import numpy as np\n", + "\n", + "def cost_function(x, y):\n", + " return 0.9\n", + "\n", + "def tpe_objective_fn(trial):\n", + " num_steps = 5\n", + " num_heat_sources = 7\n", + " lb = [0, 1, 2, 3, 4, 5, 6]\n", + " ub = [4, 5, 6, 7, 8, 9, 10]\n", + " chosen_parameters = []\n", + " for step in range(num_steps):\n", + " for idx in range(num_heat_sources):\n", + " chosen_parameters.append(\n", + " trial.suggest_float(f\"tech_param_{step}_{idx}\", lb[idx], ub[idx])\n", + " )\n", + "\n", + " cost = cost_function(\n", + " chosen_parameters,\n", + " num_steps,\n", + " )\n", + " return cost\n", + "\n", + "if __name__ == \"__main__\":\n", + " study = optuna.create_study(\n", + " storage=\"sqlite:///db.sqlite3\", # Specify the storage URL here.\n", + " study_name=\"mdbugb\"\n", + " )\n", + " study.optimize(tpe_objective_fn, n_trials=7)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c66fec2-3e46-46c4-bc55-e784d237a3e5", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Untitled.ipynb b/Untitled.ipynb new file mode 100644 index 00000000..2307d508 --- /dev/null +++ b/Untitled.ipynb @@ -0,0 +1,85 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "c45a38b9-08a7-4ad1-85eb-722b50240f2d", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\victo\\AppData\\Local\\Programs\\Python\\Python311\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n", + "[I 2023-10-21 17:21:34,373] A new study created in RDB with name: quadratic-simple3\n", + "[I 2023-10-21 17:21:34,461] Trial 0 finished with value: 4357.102948257252 and parameters: {'x': 66.0007799064318, 'y': 1}. Best is trial 0 with value: 4357.102948257252.\n", + "[W 2023-10-21 17:21:34,508] Trial 1 failed with parameters: {'x': -21.941587885601493, 'y': -1} because of the following error: The value None could not be cast to float..\n", + "[W 2023-10-21 17:21:34,509] Trial 1 failed with value None.\n", + "[I 2023-10-21 17:21:34,566] Trial 2 finished with value: 2536.6789394289817 and parameters: {'x': 50.37538029066363, 'y': -1}. Best is trial 2 with value: 2536.6789394289817.\n", + "[W 2023-10-21 17:21:34,612] Trial 3 failed with parameters: {'x': -96.83015025022239, 'y': 0} because of the following error: The value None could not be cast to float..\n", + "[W 2023-10-21 17:21:34,613] Trial 3 failed with value None.\n", + "[I 2023-10-21 17:21:34,670] Trial 4 finished with value: 3591.471500742954 and parameters: {'x': -59.920543228036195, 'y': 1}. Best is trial 2 with value: 2536.6789394289817.\n", + "[W 2023-10-21 17:21:34,717] Trial 5 failed with parameters: {'x': -65.11319825521136, 'y': 1} because of the following error: The value None could not be cast to float..\n", + "[W 2023-10-21 17:21:34,717] Trial 5 failed with value None.\n", + "[I 2023-10-21 17:21:34,769] Trial 6 finished with value: 9478.234748213168 and parameters: {'x': -97.35622603723486, 'y': 0}. Best is trial 2 with value: 2536.6789394289817.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best value: 2536.6789394289817 (params: {'x': 50.37538029066363, 'y': -1})\n" + ] + } + ], + "source": [ + "import optuna\n", + "\n", + "def objective(trial):\n", + " x = trial.suggest_float(\"x\", -100, 100)\n", + " y = trial.suggest_categorical(\"y\", [-1, 0, 1])\n", + " if trial._trial_id % 2:\n", + " return None\n", + " return x**2 + y\n", + "\n", + "if __name__ == \"__main__\":\n", + " study = optuna.create_study(\n", + " storage=\"sqlite:///db.sqlite3\", # Specify the storage URL here.\n", + " study_name=\"quadratic-simple3\"\n", + " )\n", + " study.optimize(objective, n_trials=7)\n", + " print(f\"Best value: {study.best_value} (params: {study.best_params})\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fedfab0b-c074-4d94-b63f-e6ee7df8e9e0", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index 32e2a431..f10d051f 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -165,7 +165,7 @@ def create_app( response.status = 500 return {"reason": "Failed to load the new study"} - note.transfer_notes(storage, src_study, dst_study) + note.copy_notes(storage, src_study, dst_study) storage.delete_study(src_study._study_id) response.status = 201 return serialize_study_summary(new_study_summary) @@ -177,7 +177,6 @@ def create_app( delete_all_artifacts(artifact_store, storage, study_id) try: - note.delete_study_notes(storage, study_id) storage.delete_study(study_id) except KeyError: response.status = 404 # Not found diff --git a/optuna_dashboard/_note.py b/optuna_dashboard/_note.py index 57135211..507e3d8f 100644 --- a/optuna_dashboard/_note.py +++ b/optuna_dashboard/_note.py @@ -110,21 +110,17 @@ def note_str_key_prefix(trial_id: Optional[int]) -> str: return f"dashboard:{trial_id}:note_str:" -def transfer_notes(storage: BaseStorage, src_study: optuna.Study, dst_study: optuna.Study) -> None: +def copy_notes(storage: BaseStorage, src_study: optuna.Study, dst_study: optuna.Study) -> None: system_attrs = storage.get_study_system_attrs(study_id=src_study._study_id) - def transfer(src_trial_id: Optional[int], dst_trial_id: Optional[int]) -> None: - note = get_note_from_system_attrs(system_attrs, src_trial_id)["body"] - save_note_with_version(storage, dst_study._study_id, dst_trial_id, 0, note) - delete_notes(storage, src_study._study_id, src_trial_id) - - # Transfer individual trial notes + # Copy individual trial notes for src_trial, dst_trial in zip(src_study.get_trials(), dst_study.get_trials()): - transfer(src_trial._trial_id, dst_trial._trial_id) + note = get_note_from_system_attrs(system_attrs, src_trial._trial_id)["body"] + save_note_with_version(storage, dst_study._study_id, dst_trial._trial_id, 0, note) - # Transfer study note - NO_SRC_TRIAL, NO_DST_TRIAL = None, None - transfer(NO_SRC_TRIAL, NO_DST_TRIAL) + # Copy study note + note = get_note_from_system_attrs(system_attrs, None)["body"] + save_note_with_version(storage, dst_study._study_id, None, 0, note) def get_note_from_system_attrs(system_attrs: dict[str, Any], trial_id: Optional[int]) -> NoteType: @@ -149,17 +145,6 @@ def version_is_incremented( return req_note_ver == db_note_ver + 1 -def all_trial_notes( - storage: BaseStorage, study_id: int, trial_id: Optional[int] -) -> dict[str, str]: - all_note_attrs: dict[str, str] = { - key: value - for key, value in storage.get_study_system_attrs(study_id).items() - if key.startswith(note_str_key_prefix(trial_id)) - } - return all_note_attrs - - def save_note_with_version( storage: BaseStorage, study_id: int, trial_id: Optional[int], ver: int, body: str ) -> None: @@ -170,26 +155,16 @@ def save_note_with_version( storage.set_study_system_attr(study_id, k, v) # Clear previous messages - all_note_attrs = all_trial_notes(storage, study_id, trial_id) + all_note_attrs: dict[str, str] = { + key: value + for key, value in storage.get_study_system_attrs(study_id).items() + if key.startswith(note_str_key_prefix(trial_id)) + } if len(all_note_attrs) > len(attrs): for i in range(len(attrs), len(all_note_attrs)): storage.set_study_system_attr(study_id, f"{note_str_key_prefix(trial_id)}{i}", "") -def delete_study_notes(storage: BaseStorage, study_id: int) -> None: - for trial in storage.get_all_trials(study_id): - delete_notes(storage, study_id, trial._trial_id) - - delete_notes(storage, study_id, None) - - -def delete_notes(storage: BaseStorage, study_id: int, trial_id: Optional[int]) -> None: - all_note_attrs = all_trial_notes(storage, study_id, trial_id) - - for i in range(len(all_note_attrs)): - storage.set_study_system_attr(study_id, f"{note_str_key_prefix(trial_id)}{i}", "") - - def split_body(note_str: str, trial_id: Optional[int]) -> dict[str, str]: note_len = len(note_str) attrs = {} diff --git a/python_tests/test_note.py b/python_tests/test_note.py index 55c8f610..592cf3e7 100644 --- a/python_tests/test_note.py +++ b/python_tests/test_note.py @@ -54,50 +54,7 @@ class NoteTestCase(TestCase): self.assertEqual(note_dict["body"], body) self.assertEqual(note_dict["version"], expected_ver) - def test_delete_notes_trial(self) -> None: - study = optuna.create_study() - trials = [ - study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) for _ in range(2) - ] - storage = study._storage - - notes = ["trial 0", "trial 1"] - for trial, body in zip(trials, notes): - with self.subTest(body): - save_note(trial, body) - - self.assertEqual(get_note(trial), body) - - note.delete_notes(storage, study._study_id, trial._trial_id) - - self.assertEqual(get_note(trial), "") - - def test_delete_notes_study(self) -> None: - study = optuna.create_study() - trials = [ - study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) for _ in range(2) - ] - storage = study._storage - - notes = ["trial 0", "trial 1"] - for trial, body in zip(trials, notes): - with self.subTest(body): - save_note(trial, body) - - actual = get_note(trial) - self.assertEqual(actual, body) - - save_note(study, "Study note") - actual = get_note(study) - self.assertEqual(actual, "Study note") - - note.delete_study_notes(storage, study._study_id) - - for trial in trials: - self.assertEqual(get_note(trial), "") - self.assertEqual(get_note(study), "") - - def test_transfer_notes(self) -> None: + def test_copy_notes(self) -> None: old_study = optuna.create_study() old_trials = [ old_study.ask({"x1": optuna.distributions.FloatDistribution(0, 10)}) for _ in range(2) @@ -112,12 +69,7 @@ class NoteTestCase(TestCase): new_study = optuna.create_study(storage=storage, directions=old_study.directions) new_study.add_trials(old_study.get_trials(deepcopy=False)) - note.transfer_notes(storage, old_study, new_study) - - for old_trial in old_trials: - self.assertEqual(get_note(old_trial), "") - self.assertEqual(get_note(old_study), "") - + note.copy_notes(storage, old_study, new_study) system_attrs = new_study._storage.get_study_system_attrs(new_study._study_id) for new_trial, body in zip(new_study.get_trials(), notes): actual = note.get_note_from_system_attrs(system_attrs, new_trial._trial_id) From 6873b3b9ba576125e6ff737a4cd0af41a469bb33 Mon Sep 17 00:00:00 2001 From: Victoria A <52001888+adjeiv@users.noreply.github.com> Date: Tue, 28 Nov 2023 14:02:44 +0000 Subject: [PATCH 130/176] Remove added files --- .ipynb_checkpoints/MD Bug-checkpoint.ipynb | 104 ------------------- .ipynb_checkpoints/Untitled-checkpoint.ipynb | 6 -- MD Bug.ipynb | 87 ---------------- Untitled.ipynb | 85 --------------- 4 files changed, 282 deletions(-) delete mode 100644 .ipynb_checkpoints/MD Bug-checkpoint.ipynb delete mode 100644 .ipynb_checkpoints/Untitled-checkpoint.ipynb delete mode 100644 MD Bug.ipynb delete mode 100644 Untitled.ipynb diff --git a/.ipynb_checkpoints/MD Bug-checkpoint.ipynb b/.ipynb_checkpoints/MD Bug-checkpoint.ipynb deleted file mode 100644 index 980a307a..00000000 --- a/.ipynb_checkpoints/MD Bug-checkpoint.ipynb +++ /dev/null @@ -1,104 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 5, - "id": "32593824-793e-486e-8494-7f18f73ae5ee", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[I 2023-11-18 12:06:31,357] A new study created in RDB with name: mdbug2\n", - "[W 2023-11-18 12:06:31,454] Trial 0 failed with parameters: {} because of the following error: TypeError(\"tpe_objective_fn() missing 1 required positional argument: 'observations'\").\n", - "Traceback (most recent call last):\n", - " File \"C:\\Users\\victo\\Documents\\OpenSource\\optuna_workspace\\optuna\\optuna\\study\\_optimize.py\", line 200, in _run_trial\n", - " value_or_values = func(trial)\n", - " ^^^^^^^^^^^\n", - "TypeError: tpe_objective_fn() missing 1 required positional argument: 'observations'\n", - "[W 2023-11-18 12:06:31,457] Trial 0 failed with value None.\n" - ] - }, - { - "ename": "TypeError", - "evalue": "tpe_objective_fn() missing 1 required positional argument: 'observations'", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[5], line 30\u001b[0m\n\u001b[0;32m 25\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;18m__name__\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m__main__\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 26\u001b[0m study \u001b[38;5;241m=\u001b[39m optuna\u001b[38;5;241m.\u001b[39mcreate_study(\n\u001b[0;32m 27\u001b[0m storage\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msqlite:///db.sqlite3\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;66;03m# Specify the storage URL here.\u001b[39;00m\n\u001b[0;32m 28\u001b[0m study_name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmdbug2\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 29\u001b[0m )\n\u001b[1;32m---> 30\u001b[0m \u001b[43mstudy\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43moptimize\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtpe_objective_fn\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mn_trials\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m7\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[0;32m 31\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mBest value: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstudy\u001b[38;5;241m.\u001b[39mbest_value\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m (params: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstudy\u001b[38;5;241m.\u001b[39mbest_params\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m)\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", - "File \u001b[1;32m~\\Documents\\OpenSource\\optuna_workspace\\optuna\\optuna\\study\\study.py:451\u001b[0m, in \u001b[0;36mStudy.optimize\u001b[1;34m(self, func, n_trials, timeout, n_jobs, catch, callbacks, gc_after_trial, show_progress_bar)\u001b[0m\n\u001b[0;32m 348\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21moptimize\u001b[39m(\n\u001b[0;32m 349\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[0;32m 350\u001b[0m func: ObjectiveFuncType,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 357\u001b[0m show_progress_bar: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[0;32m 358\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m 359\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Optimize an objective function.\u001b[39;00m\n\u001b[0;32m 360\u001b[0m \n\u001b[0;32m 361\u001b[0m \u001b[38;5;124;03m Optimization is done by choosing a suitable set of hyperparameter values from a given\u001b[39;00m\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 449\u001b[0m \u001b[38;5;124;03m If nested invocation of this method occurs.\u001b[39;00m\n\u001b[0;32m 450\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m--> 451\u001b[0m \u001b[43m_optimize\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 452\u001b[0m \u001b[43m \u001b[49m\u001b[43mstudy\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[0;32m 453\u001b[0m \u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 454\u001b[0m \u001b[43m \u001b[49m\u001b[43mn_trials\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mn_trials\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 455\u001b[0m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mtimeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 456\u001b[0m \u001b[43m \u001b[49m\u001b[43mn_jobs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mn_jobs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 457\u001b[0m \u001b[43m \u001b[49m\u001b[43mcatch\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mtuple\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mcatch\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43misinstance\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mcatch\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mIterable\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[43mcatch\u001b[49m\u001b[43m,\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 458\u001b[0m \u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcallbacks\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 459\u001b[0m \u001b[43m \u001b[49m\u001b[43mgc_after_trial\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mgc_after_trial\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 460\u001b[0m \u001b[43m \u001b[49m\u001b[43mshow_progress_bar\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mshow_progress_bar\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 461\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[1;32m~\\Documents\\OpenSource\\optuna_workspace\\optuna\\optuna\\study\\_optimize.py:66\u001b[0m, in \u001b[0;36m_optimize\u001b[1;34m(study, func, n_trials, timeout, n_jobs, catch, callbacks, gc_after_trial, show_progress_bar)\u001b[0m\n\u001b[0;32m 64\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 65\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n_jobs \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[1;32m---> 66\u001b[0m \u001b[43m_optimize_sequential\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 67\u001b[0m \u001b[43m \u001b[49m\u001b[43mstudy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 68\u001b[0m \u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 69\u001b[0m \u001b[43m \u001b[49m\u001b[43mn_trials\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 70\u001b[0m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 71\u001b[0m \u001b[43m \u001b[49m\u001b[43mcatch\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 72\u001b[0m \u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 73\u001b[0m \u001b[43m \u001b[49m\u001b[43mgc_after_trial\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 74\u001b[0m \u001b[43m \u001b[49m\u001b[43mreseed_sampler_rng\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m 75\u001b[0m \u001b[43m \u001b[49m\u001b[43mtime_start\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[0;32m 76\u001b[0m \u001b[43m \u001b[49m\u001b[43mprogress_bar\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mprogress_bar\u001b[49m\u001b[43m,\u001b[49m\n\u001b[0;32m 77\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 78\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 79\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n_jobs \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m:\n", - "File \u001b[1;32m~\\Documents\\OpenSource\\optuna_workspace\\optuna\\optuna\\study\\_optimize.py:163\u001b[0m, in \u001b[0;36m_optimize_sequential\u001b[1;34m(study, func, n_trials, timeout, catch, callbacks, gc_after_trial, reseed_sampler_rng, time_start, progress_bar)\u001b[0m\n\u001b[0;32m 160\u001b[0m \u001b[38;5;28;01mbreak\u001b[39;00m\n\u001b[0;32m 162\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 163\u001b[0m frozen_trial \u001b[38;5;241m=\u001b[39m \u001b[43m_run_trial\u001b[49m\u001b[43m(\u001b[49m\u001b[43mstudy\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcatch\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 164\u001b[0m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[0;32m 165\u001b[0m \u001b[38;5;66;03m# The following line mitigates memory problems that can be occurred in some\u001b[39;00m\n\u001b[0;32m 166\u001b[0m \u001b[38;5;66;03m# environments (e.g., services that use computing containers such as GitHub Actions).\u001b[39;00m\n\u001b[0;32m 167\u001b[0m \u001b[38;5;66;03m# Please refer to the following PR for further details:\u001b[39;00m\n\u001b[0;32m 168\u001b[0m \u001b[38;5;66;03m# https://github.com/optuna/optuna/pull/325.\u001b[39;00m\n\u001b[0;32m 169\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m gc_after_trial:\n", - "File \u001b[1;32m~\\Documents\\OpenSource\\optuna_workspace\\optuna\\optuna\\study\\_optimize.py:251\u001b[0m, in \u001b[0;36m_run_trial\u001b[1;34m(study, func, catch)\u001b[0m\n\u001b[0;32m 244\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;28;01mFalse\u001b[39;00m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mShould not reach.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 246\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m (\n\u001b[0;32m 247\u001b[0m frozen_trial\u001b[38;5;241m.\u001b[39mstate \u001b[38;5;241m==\u001b[39m TrialState\u001b[38;5;241m.\u001b[39mFAIL\n\u001b[0;32m 248\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m func_err \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[0;32m 249\u001b[0m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(func_err, catch)\n\u001b[0;32m 250\u001b[0m ):\n\u001b[1;32m--> 251\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m func_err\n\u001b[0;32m 252\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m frozen_trial\n", - "File \u001b[1;32m~\\Documents\\OpenSource\\optuna_workspace\\optuna\\optuna\\study\\_optimize.py:200\u001b[0m, in \u001b[0;36m_run_trial\u001b[1;34m(study, func, catch)\u001b[0m\n\u001b[0;32m 198\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m get_heartbeat_thread(trial\u001b[38;5;241m.\u001b[39m_trial_id, study\u001b[38;5;241m.\u001b[39m_storage):\n\u001b[0;32m 199\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m--> 200\u001b[0m value_or_values \u001b[38;5;241m=\u001b[39m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtrial\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 201\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m exceptions\u001b[38;5;241m.\u001b[39mTrialPruned \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[0;32m 202\u001b[0m \u001b[38;5;66;03m# TODO(mamu): Handle multi-objective cases.\u001b[39;00m\n\u001b[0;32m 203\u001b[0m state \u001b[38;5;241m=\u001b[39m TrialState\u001b[38;5;241m.\u001b[39mPRUNED\n", - "\u001b[1;31mTypeError\u001b[0m: tpe_objective_fn() missing 1 required positional argument: 'observations'" - ] - } - ], - "source": [ - "import optuna\n", - "import numpy as np\n", - "\n", - "def cost_function(x, y):\n", - " return np.rand(0, 1)\n", - "\n", - "def tpe_objective_fn(trial):\n", - " num_steps = 5\n", - " num_heat_sources = 7\n", - " lb = [0, 1, 2, 3, 4, 5, 6]\n", - " ub = [4, 5, 6, 7, 8, 9, 10]\n", - " chosen_parameters = []\n", - " for step in range(num_steps):\n", - " for idx in range(num_heat_sources):\n", - " chosen_parameters.append(\n", - " trial.suggest_float(f\"tech_param_{step}_{idx}\", lb[idx], ub[idx])\n", - " )\n", - "\n", - " cost = cost_function(\n", - " chosen_parameters,\n", - " num_steps,\n", - " )\n", - " return cost\n", - "\n", - "if __name__ == \"__main__\":\n", - " study = optuna.create_study(\n", - " storage=\"sqlite:///db.sqlite3\", # Specify the storage URL here.\n", - " study_name=\"mdbug2\"\n", - " )\n", - " study.optimize(tpe_objective_fn, n_trials=7)\n", - " print(f\"Best value: {study.best_value} (params: {study.best_params})\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2c66fec2-3e46-46c4-bc55-e784d237a3e5", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.4" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/.ipynb_checkpoints/Untitled-checkpoint.ipynb b/.ipynb_checkpoints/Untitled-checkpoint.ipynb deleted file mode 100644 index 363fcab7..00000000 --- a/.ipynb_checkpoints/Untitled-checkpoint.ipynb +++ /dev/null @@ -1,6 +0,0 @@ -{ - "cells": [], - "metadata": {}, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/MD Bug.ipynb b/MD Bug.ipynb deleted file mode 100644 index 75b0ea27..00000000 --- a/MD Bug.ipynb +++ /dev/null @@ -1,87 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 10, - "id": "32593824-793e-486e-8494-7f18f73ae5ee", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[I 2023-11-18 12:13:05,829] A new study created in RDB with name: mdbugb\n", - "[I 2023-11-18 12:13:06,187] Trial 0 finished with value: 0.9 and parameters: {'tech_param_0_0': 0.9863311667179313, 'tech_param_0_1': 2.9722067965404126, 'tech_param_0_2': 5.479907499743063, 'tech_param_0_3': 6.193147335867399, 'tech_param_0_4': 6.44329845141624, 'tech_param_0_5': 6.957704292533055, 'tech_param_0_6': 6.028116976338117, 'tech_param_1_0': 0.8528770906850203, 'tech_param_1_1': 2.8845780424110843, 'tech_param_1_2': 2.205086423835636, 'tech_param_1_3': 5.8965466201793735, 'tech_param_1_4': 6.765916728183967, 'tech_param_1_5': 5.216406754175934, 'tech_param_1_6': 9.921684209548362, 'tech_param_2_0': 3.9278254745655903, 'tech_param_2_1': 2.8605537473963465, 'tech_param_2_2': 4.466559550814772, 'tech_param_2_3': 4.838464946220083, 'tech_param_2_4': 4.026691756550308, 'tech_param_2_5': 5.3788556566702805, 'tech_param_2_6': 6.675770800145582, 'tech_param_3_0': 3.031562395355724, 'tech_param_3_1': 3.7349707362119777, 'tech_param_3_2': 5.473994937262473, 'tech_param_3_3': 6.881019240236522, 'tech_param_3_4': 6.120001351513482, 'tech_param_3_5': 6.036086717431755, 'tech_param_3_6': 9.160127979158023, 'tech_param_4_0': 2.758775673013467, 'tech_param_4_1': 1.0680707015794786, 'tech_param_4_2': 3.3292082013802666, 'tech_param_4_3': 6.687649735544305, 'tech_param_4_4': 7.443681016669299, 'tech_param_4_5': 7.162694801047951, 'tech_param_4_6': 9.999288376422493}. Best is trial 0 with value: 0.9.\n", - "[I 2023-11-18 12:13:06,501] Trial 1 finished with value: 0.9 and parameters: {'tech_param_0_0': 1.7140076646332538, 'tech_param_0_1': 3.2316252813476325, 'tech_param_0_2': 4.306343376567941, 'tech_param_0_3': 4.61192706012931, 'tech_param_0_4': 5.988119724240175, 'tech_param_0_5': 6.9327357154708285, 'tech_param_0_6': 6.53997726319049, 'tech_param_1_0': 0.3905783000188636, 'tech_param_1_1': 2.76165385255339, 'tech_param_1_2': 2.6085969410537855, 'tech_param_1_3': 4.749816941187031, 'tech_param_1_4': 4.846918571308436, 'tech_param_1_5': 6.254401319703383, 'tech_param_1_6': 7.8373063069546935, 'tech_param_2_0': 2.91103305557099, 'tech_param_2_1': 4.471679319725785, 'tech_param_2_2': 3.5984067288829946, 'tech_param_2_3': 3.470290937133842, 'tech_param_2_4': 7.799315424111551, 'tech_param_2_5': 7.826822170149293, 'tech_param_2_6': 8.849116163752687, 'tech_param_3_0': 3.5380619781168816, 'tech_param_3_1': 3.653451480021751, 'tech_param_3_2': 3.47434126199421, 'tech_param_3_3': 4.443348897470383, 'tech_param_3_4': 7.045847902681517, 'tech_param_3_5': 6.2594143301415475, 'tech_param_3_6': 7.979774488188764, 'tech_param_4_0': 1.0325150795599196, 'tech_param_4_1': 3.4448235367545186, 'tech_param_4_2': 3.3149959756969167, 'tech_param_4_3': 4.819419990796481, 'tech_param_4_4': 7.240391392837244, 'tech_param_4_5': 6.412867172919929, 'tech_param_4_6': 7.784828203240426}. Best is trial 0 with value: 0.9.\n", - "[I 2023-11-18 12:13:06,830] Trial 2 finished with value: 0.9 and parameters: {'tech_param_0_0': 3.7285153030023213, 'tech_param_0_1': 2.967995773245595, 'tech_param_0_2': 2.1806768067032025, 'tech_param_0_3': 4.166177958423523, 'tech_param_0_4': 7.340303214415031, 'tech_param_0_5': 8.466190260210265, 'tech_param_0_6': 8.838603377394787, 'tech_param_1_0': 1.7630336762562222, 'tech_param_1_1': 1.1619500439780515, 'tech_param_1_2': 3.4038384919507747, 'tech_param_1_3': 6.650347896582192, 'tech_param_1_4': 7.7764728822600055, 'tech_param_1_5': 8.639229218462436, 'tech_param_1_6': 7.113069116576301, 'tech_param_2_0': 3.9931082904645394, 'tech_param_2_1': 3.1473900771322443, 'tech_param_2_2': 4.8958785737518475, 'tech_param_2_3': 4.135490374890335, 'tech_param_2_4': 4.642792333607787, 'tech_param_2_5': 6.804997337959285, 'tech_param_2_6': 7.093905518212265, 'tech_param_3_0': 3.7794491588384793, 'tech_param_3_1': 2.9200030292104078, 'tech_param_3_2': 2.193269350123763, 'tech_param_3_3': 4.4228612589114915, 'tech_param_3_4': 6.768417418097517, 'tech_param_3_5': 7.833147977002255, 'tech_param_3_6': 6.043265945471216, 'tech_param_4_0': 1.239068042928749, 'tech_param_4_1': 3.0223635344959376, 'tech_param_4_2': 4.542741924432429, 'tech_param_4_3': 5.527626147325485, 'tech_param_4_4': 6.41989159985463, 'tech_param_4_5': 5.957849401921937, 'tech_param_4_6': 8.220767432559985}. Best is trial 0 with value: 0.9.\n", - "[I 2023-11-18 12:13:07,175] Trial 3 finished with value: 0.9 and parameters: {'tech_param_0_0': 2.3406080881578437, 'tech_param_0_1': 2.20431580814314, 'tech_param_0_2': 2.315142042718149, 'tech_param_0_3': 3.153313622905333, 'tech_param_0_4': 6.434868333320647, 'tech_param_0_5': 8.010675478290011, 'tech_param_0_6': 9.795451780231673, 'tech_param_1_0': 3.893698909905487, 'tech_param_1_1': 3.448571655312496, 'tech_param_1_2': 2.7450589731117088, 'tech_param_1_3': 5.2194903883501, 'tech_param_1_4': 7.236541468886768, 'tech_param_1_5': 6.823921860165192, 'tech_param_1_6': 9.616564141906174, 'tech_param_2_0': 0.39123634971167975, 'tech_param_2_1': 2.39626451267548, 'tech_param_2_2': 3.0034883416963494, 'tech_param_2_3': 6.376136926494553, 'tech_param_2_4': 7.218911507033649, 'tech_param_2_5': 6.750947297023547, 'tech_param_2_6': 8.736332931103936, 'tech_param_3_0': 0.6196213625340548, 'tech_param_3_1': 3.387324252791688, 'tech_param_3_2': 4.183149086442488, 'tech_param_3_3': 5.183411467900889, 'tech_param_3_4': 6.267654527841776, 'tech_param_3_5': 5.549621056761763, 'tech_param_3_6': 7.672308955901496, 'tech_param_4_0': 1.246623944346387, 'tech_param_4_1': 4.456831294515673, 'tech_param_4_2': 2.0724147884920074, 'tech_param_4_3': 5.379419267307556, 'tech_param_4_4': 7.112725631815934, 'tech_param_4_5': 8.87353147211932, 'tech_param_4_6': 7.693639964574908}. Best is trial 0 with value: 0.9.\n", - "[I 2023-11-18 12:13:07,487] Trial 4 finished with value: 0.9 and parameters: {'tech_param_0_0': 3.1068581046485, 'tech_param_0_1': 2.9287122006776443, 'tech_param_0_2': 5.4057937949623085, 'tech_param_0_3': 3.157484488408773, 'tech_param_0_4': 4.690948899704123, 'tech_param_0_5': 8.145312132270456, 'tech_param_0_6': 6.089344894219481, 'tech_param_1_0': 2.9926885191436527, 'tech_param_1_1': 3.176774691423508, 'tech_param_1_2': 2.7721710618333737, 'tech_param_1_3': 4.67762193566592, 'tech_param_1_4': 4.012363168044638, 'tech_param_1_5': 5.659401760473942, 'tech_param_1_6': 8.830934616907934, 'tech_param_2_0': 1.661839241732621, 'tech_param_2_1': 2.7405670060757923, 'tech_param_2_2': 2.100697848478035, 'tech_param_2_3': 6.253340195464311, 'tech_param_2_4': 6.976724037705612, 'tech_param_2_5': 5.510598362317128, 'tech_param_2_6': 7.522477241180887, 'tech_param_3_0': 2.4453135979613685, 'tech_param_3_1': 1.695095408763359, 'tech_param_3_2': 3.102101960127642, 'tech_param_3_3': 4.087680321929806, 'tech_param_3_4': 6.130088543099206, 'tech_param_3_5': 7.350026673357341, 'tech_param_3_6': 8.078588257404482, 'tech_param_4_0': 3.4813120281559478, 'tech_param_4_1': 3.6566237591430295, 'tech_param_4_2': 4.615726645672355, 'tech_param_4_3': 3.5688464780387226, 'tech_param_4_4': 4.212091370332146, 'tech_param_4_5': 6.790648339845854, 'tech_param_4_6': 9.793540661498884}. Best is trial 0 with value: 0.9.\n", - "[I 2023-11-18 12:13:07,804] Trial 5 finished with value: 0.9 and parameters: {'tech_param_0_0': 3.7955449152390734, 'tech_param_0_1': 3.5879051604064425, 'tech_param_0_2': 5.047936105941167, 'tech_param_0_3': 6.409387696471882, 'tech_param_0_4': 6.139796193716612, 'tech_param_0_5': 5.818035000269156, 'tech_param_0_6': 6.77671788266593, 'tech_param_1_0': 3.538002063564025, 'tech_param_1_1': 4.653916400756065, 'tech_param_1_2': 5.139067082232518, 'tech_param_1_3': 5.258612796259207, 'tech_param_1_4': 5.762995511794475, 'tech_param_1_5': 5.914412902115752, 'tech_param_1_6': 8.821608430328544, 'tech_param_2_0': 2.8391352600362043, 'tech_param_2_1': 1.3147834382265242, 'tech_param_2_2': 5.712530324883787, 'tech_param_2_3': 3.9982127746896365, 'tech_param_2_4': 7.59784603286235, 'tech_param_2_5': 8.249304067800995, 'tech_param_2_6': 9.13797124898645, 'tech_param_3_0': 3.4218288637879235, 'tech_param_3_1': 2.8432006830690506, 'tech_param_3_2': 3.705582741959596, 'tech_param_3_3': 4.932782214722957, 'tech_param_3_4': 5.397522930636624, 'tech_param_3_5': 7.665389302007817, 'tech_param_3_6': 8.161440460609116, 'tech_param_4_0': 0.9002808033064547, 'tech_param_4_1': 2.8214804227095938, 'tech_param_4_2': 4.209001287953269, 'tech_param_4_3': 3.4276241669947267, 'tech_param_4_4': 5.65920280643175, 'tech_param_4_5': 7.0296445059400385, 'tech_param_4_6': 6.58394027927913}. Best is trial 0 with value: 0.9.\n", - "[I 2023-11-18 12:13:08,436] Trial 6 finished with value: 0.9 and parameters: {'tech_param_0_0': 2.1661335557317467, 'tech_param_0_1': 2.10482781209877, 'tech_param_0_2': 3.5267055048813605, 'tech_param_0_3': 3.3403923781628193, 'tech_param_0_4': 7.251793183308415, 'tech_param_0_5': 5.419077835893116, 'tech_param_0_6': 6.92337803745288, 'tech_param_1_0': 2.248588320589992, 'tech_param_1_1': 1.5496877038644339, 'tech_param_1_2': 3.034290526391054, 'tech_param_1_3': 5.70430130433426, 'tech_param_1_4': 4.755971043039262, 'tech_param_1_5': 6.032107463009746, 'tech_param_1_6': 6.485937845252712, 'tech_param_2_0': 1.1198667631159256, 'tech_param_2_1': 4.443832638453198, 'tech_param_2_2': 4.029162583096888, 'tech_param_2_3': 4.421249989146505, 'tech_param_2_4': 7.266267320462586, 'tech_param_2_5': 7.272268040989632, 'tech_param_2_6': 9.34608451054512, 'tech_param_3_0': 3.5400627448638406, 'tech_param_3_1': 1.0291771457580832, 'tech_param_3_2': 3.001266350018537, 'tech_param_3_3': 3.83986701735892, 'tech_param_3_4': 4.21380354332598, 'tech_param_3_5': 5.7510339129863555, 'tech_param_3_6': 9.666174111722901, 'tech_param_4_0': 0.25936572770098243, 'tech_param_4_1': 1.0411384740076457, 'tech_param_4_2': 3.6068625395816722, 'tech_param_4_3': 4.778931228821068, 'tech_param_4_4': 5.259093795072825, 'tech_param_4_5': 8.171709511787906, 'tech_param_4_6': 9.580347111957641}. Best is trial 0 with value: 0.9.\n" - ] - } - ], - "source": [ - "import optuna\n", - "import numpy as np\n", - "\n", - "def cost_function(x, y):\n", - " return 0.9\n", - "\n", - "def tpe_objective_fn(trial):\n", - " num_steps = 5\n", - " num_heat_sources = 7\n", - " lb = [0, 1, 2, 3, 4, 5, 6]\n", - " ub = [4, 5, 6, 7, 8, 9, 10]\n", - " chosen_parameters = []\n", - " for step in range(num_steps):\n", - " for idx in range(num_heat_sources):\n", - " chosen_parameters.append(\n", - " trial.suggest_float(f\"tech_param_{step}_{idx}\", lb[idx], ub[idx])\n", - " )\n", - "\n", - " cost = cost_function(\n", - " chosen_parameters,\n", - " num_steps,\n", - " )\n", - " return cost\n", - "\n", - "if __name__ == \"__main__\":\n", - " study = optuna.create_study(\n", - " storage=\"sqlite:///db.sqlite3\", # Specify the storage URL here.\n", - " study_name=\"mdbugb\"\n", - " )\n", - " study.optimize(tpe_objective_fn, n_trials=7)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2c66fec2-3e46-46c4-bc55-e784d237a3e5", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.4" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/Untitled.ipynb b/Untitled.ipynb deleted file mode 100644 index 2307d508..00000000 --- a/Untitled.ipynb +++ /dev/null @@ -1,85 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "c45a38b9-08a7-4ad1-85eb-722b50240f2d", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\victo\\AppData\\Local\\Programs\\Python\\Python311\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n", - "[I 2023-10-21 17:21:34,373] A new study created in RDB with name: quadratic-simple3\n", - "[I 2023-10-21 17:21:34,461] Trial 0 finished with value: 4357.102948257252 and parameters: {'x': 66.0007799064318, 'y': 1}. Best is trial 0 with value: 4357.102948257252.\n", - "[W 2023-10-21 17:21:34,508] Trial 1 failed with parameters: {'x': -21.941587885601493, 'y': -1} because of the following error: The value None could not be cast to float..\n", - "[W 2023-10-21 17:21:34,509] Trial 1 failed with value None.\n", - "[I 2023-10-21 17:21:34,566] Trial 2 finished with value: 2536.6789394289817 and parameters: {'x': 50.37538029066363, 'y': -1}. Best is trial 2 with value: 2536.6789394289817.\n", - "[W 2023-10-21 17:21:34,612] Trial 3 failed with parameters: {'x': -96.83015025022239, 'y': 0} because of the following error: The value None could not be cast to float..\n", - "[W 2023-10-21 17:21:34,613] Trial 3 failed with value None.\n", - "[I 2023-10-21 17:21:34,670] Trial 4 finished with value: 3591.471500742954 and parameters: {'x': -59.920543228036195, 'y': 1}. Best is trial 2 with value: 2536.6789394289817.\n", - "[W 2023-10-21 17:21:34,717] Trial 5 failed with parameters: {'x': -65.11319825521136, 'y': 1} because of the following error: The value None could not be cast to float..\n", - "[W 2023-10-21 17:21:34,717] Trial 5 failed with value None.\n", - "[I 2023-10-21 17:21:34,769] Trial 6 finished with value: 9478.234748213168 and parameters: {'x': -97.35622603723486, 'y': 0}. Best is trial 2 with value: 2536.6789394289817.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Best value: 2536.6789394289817 (params: {'x': 50.37538029066363, 'y': -1})\n" - ] - } - ], - "source": [ - "import optuna\n", - "\n", - "def objective(trial):\n", - " x = trial.suggest_float(\"x\", -100, 100)\n", - " y = trial.suggest_categorical(\"y\", [-1, 0, 1])\n", - " if trial._trial_id % 2:\n", - " return None\n", - " return x**2 + y\n", - "\n", - "if __name__ == \"__main__\":\n", - " study = optuna.create_study(\n", - " storage=\"sqlite:///db.sqlite3\", # Specify the storage URL here.\n", - " study_name=\"quadratic-simple3\"\n", - " )\n", - " study.optimize(objective, n_trials=7)\n", - " print(f\"Best value: {study.best_value} (params: {study.best_params})\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fedfab0b-c074-4d94-b63f-e6ee7df8e9e0", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.4" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 60888843dba4bac175082b130947b1eecf05c5bd Mon Sep 17 00:00:00 2001 From: Contramundum Date: Wed, 29 Nov 2023 17:22:14 +0900 Subject: [PATCH 131/176] Add test on upload_artifact --- python_tests/artifact/test_backend.py | 62 +++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/python_tests/artifact/test_backend.py b/python_tests/artifact/test_backend.py index 14a470e3..0bb662aa 100644 --- a/python_tests/artifact/test_backend.py +++ b/python_tests/artifact/test_backend.py @@ -1,3 +1,5 @@ +import base64 +import json import tempfile from unittest.mock import MagicMock @@ -177,3 +179,63 @@ def test_successful_trial_artifact_retrieval() -> None: ) assert status == 200 assert body == b"dummy_content" + + +DUMMY_DATA = f"data:text/plain; charset=utf-8,{base64.b64encode(b'dummy_content').decode('utf-8')}" + + +def test_upload_artifact_invalid() -> None: + storage = optuna.storages.InMemoryStorage() + + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + + app = create_app(storage, artifact_store) + study = optuna.create_study(storage=storage) + + # Invalid: no trial + status, _, body = send_request( + app, + f"/api/artifacts/{study._study_id}/0", + "POST", + body=json.dumps({"file": DUMMY_DATA}), + content_type="application/json", + ) + assert status == 500 # TODO: This should return 400 + + # Invalid: complete trial + study.add_trial(optuna.create_trial(value=1.0, distributions={}, params={})) + trial = study.trials[-1] + status, _, body = send_request( + app, + f"/api/artifacts/{study._study_id}/{trial._trial_id}", + "POST", + body=json.dumps({"file": DUMMY_DATA}), + content_type="application/json", + ) + assert status == 400 + + +def test_upload_artifact() -> None: + storage = optuna.storages.InMemoryStorage() + + study = optuna.create_study(storage=storage) + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + + app = create_app(storage, artifact_store) + + study.add_trial(optuna.create_trial(state=optuna.trial.TrialState.RUNNING)) + trial = study.trials[-1] + status, _, body = send_request( + app, + f"/api/artifacts/{study._study_id}/{trial._trial_id}", + "POST", + body=json.dumps({"file": DUMMY_DATA}), + content_type="application/json", + ) + assert status == 201 + res = json.loads(body) + with open(f"{tmpdir}/{res['artifact_id']}", "r") as f: + data = f.read() + assert data == "dummy_content" From 019859e20b52b7efa697033838c71bd65fe83ed2 Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Wed, 29 Nov 2023 18:16:23 +0900 Subject: [PATCH 132/176] Use WaveSurfer instead of audio tag. --- .../ts/components/ArtifactCardMedia.tsx | 10 ++- .../components/WaveSurferArtifactViewer.tsx | 79 +++++++++++++++++++ package-lock.json | 13 ++- package.json | 3 +- 4 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx diff --git a/optuna_dashboard/ts/components/ArtifactCardMedia.tsx b/optuna_dashboard/ts/components/ArtifactCardMedia.tsx index 282b7432..fecef837 100644 --- a/optuna_dashboard/ts/components/ArtifactCardMedia.tsx +++ b/optuna_dashboard/ts/components/ArtifactCardMedia.tsx @@ -3,6 +3,7 @@ import { ThreejsArtifactViewer, isThreejsArtifact, } from "./ThreejsArtifactViewer" +import { WaveSurferArtifactViewer } from "./WaveSurferArtifactViewer" import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile" import { CardMedia, Box } from "@mui/material" @@ -43,9 +44,12 @@ export const ArtifactCardMedia: FC<{ alignItems: "center", }} > - + ) } else if (artifact.mimetype.startsWith("image")) { diff --git a/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx b/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx new file mode 100644 index 00000000..44dc6455 --- /dev/null +++ b/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx @@ -0,0 +1,79 @@ +import React, { useCallback, useEffect, useState, useRef } from "react" +import WaveSurfer from "wavesurfer.js" +import { Box } from "@mui/material" + +interface WaveSurferArtifactViewerProps { + height: number + waveColor: string + progressColor: string + url: string +} + +// WaveSurfer hook +const useWavesurfer = ( + containerRef: React.MutableRefObject, + options: WaveSurferArtifactViewerProps +) => { + const [wavesurfer, setWavesurfer] = useState(null) + + // Initialize wavesurfer when the container mounts + // or any of the props change + useEffect(() => { + if (!containerRef.current) return + + const ws = WaveSurfer.create({ + ...options, + container: containerRef.current, + }) + + setWavesurfer(ws) + + return () => { + ws.destroy() + } + }, [options, containerRef]) + + return wavesurfer +} + +// Create a React component that will render wavesurfer. +// Props are wavesurfer options. +export const WaveSurferArtifactViewer: React.FC< + WaveSurferArtifactViewerProps +> = (props) => { + const containerRef = useRef(null!) + const [isPlaying, setIsPlaying] = useState(false) + const wavesurfer = useWavesurfer(containerRef, props) + + // On play button click + const onPlayClick = useCallback(() => { + if (!wavesurfer) return + wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play() + }, [wavesurfer]) + + // Initialize wavesurfer when the container mounts + // or any of the props change + useEffect(() => { + if (!wavesurfer) return + + setIsPlaying(false) + + const subscriptions = [ + wavesurfer.on("play", () => setIsPlaying(true)), + wavesurfer.on("pause", () => setIsPlaying(false)), + ] + + return () => { + subscriptions.forEach((unsub) => unsub()) + } + }, [wavesurfer]) + + return ( + +
+ + + ) +} diff --git a/package-lock.json b/package-lock.json index 409d4225..05c79bb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,8 @@ "rehype-raw": "^6.1.1", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", - "three": "^0.155.0" + "three": "^0.155.0", + "wavesurfer.js": "^7.4.12" }, "devDependencies": { "@babel/core": "^7.14.3", @@ -14824,6 +14825,11 @@ "node": ">=10.13.0" } }, + "node_modules/wavesurfer.js": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.4.12.tgz", + "integrity": "sha512-KzH4LkcOp8LECs9cOVIPBl6vsSoICKuZz+v5kh/zvxilpaVszU+QKC+4s2KEAqcCxBCecg3cNSg4RqAx278F8g==" + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -25938,6 +25944,11 @@ "graceful-fs": "^4.1.2" } }, + "wavesurfer.js": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.4.12.tgz", + "integrity": "sha512-KzH4LkcOp8LECs9cOVIPBl6vsSoICKuZz+v5kh/zvxilpaVszU+QKC+4s2KEAqcCxBCecg3cNSg4RqAx278F8g==" + }, "web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/package.json b/package.json index 3e624c56..9e6e59e9 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "rehype-raw": "^6.1.1", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", - "three": "^0.155.0" + "three": "^0.155.0", + "wavesurfer.js": "^7.4.12" }, "devDependencies": { "@babel/core": "^7.14.3", From dfc699e69cc8faf4fadfe83d77e4a52829e18ad5 Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Wed, 29 Nov 2023 18:25:00 +0900 Subject: [PATCH 133/176] Update code comments. --- .../ts/components/WaveSurferArtifactViewer.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx b/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx index 44dc6455..65d87bfb 100644 --- a/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx +++ b/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx @@ -9,15 +9,12 @@ interface WaveSurferArtifactViewerProps { url: string } -// WaveSurfer hook const useWavesurfer = ( containerRef: React.MutableRefObject, options: WaveSurferArtifactViewerProps ) => { const [wavesurfer, setWavesurfer] = useState(null) - // Initialize wavesurfer when the container mounts - // or any of the props change useEffect(() => { if (!containerRef.current) return @@ -36,8 +33,7 @@ const useWavesurfer = ( return wavesurfer } -// Create a React component that will render wavesurfer. -// Props are wavesurfer options. +// Create a React component of wavesurfer. export const WaveSurferArtifactViewer: React.FC< WaveSurferArtifactViewerProps > = (props) => { @@ -45,14 +41,11 @@ export const WaveSurferArtifactViewer: React.FC< const [isPlaying, setIsPlaying] = useState(false) const wavesurfer = useWavesurfer(containerRef, props) - // On play button click const onPlayClick = useCallback(() => { if (!wavesurfer) return wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play() }, [wavesurfer]) - // Initialize wavesurfer when the container mounts - // or any of the props change useEffect(() => { if (!wavesurfer) return From caa2d0cbbd185737ea7f4dbe8dd1268152c24aa4 Mon Sep 17 00:00:00 2001 From: Victoria A <52001888+adjeiv@users.noreply.github.com> Date: Wed, 29 Nov 2023 22:07:38 +0000 Subject: [PATCH 134/176] Move line so any exceptions are caught --- optuna_dashboard/_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index f10d051f..b91ce5f0 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -152,6 +152,7 @@ def create_app( storage=storage, study_name=dst_study_name, directions=src_study.directions ) dst_study.add_trials(src_study.get_trials(deepcopy=False)) + note.copy_notes(storage, src_study, dst_study) except DuplicatedStudyError: response.status = 400 # Bad request return {"reason": f"study_name={dst_study_name} is duplicaated"} @@ -165,7 +166,6 @@ def create_app( response.status = 500 return {"reason": "Failed to load the new study"} - note.copy_notes(storage, src_study, dst_study) storage.delete_study(src_study._study_id) response.status = 201 return serialize_study_summary(new_study_summary) From bee7c97009d10ec07a3c7d6af222b3dfd7a860ba Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Thu, 30 Nov 2023 15:24:16 +0900 Subject: [PATCH 135/176] Fix sort in rank plot --- optuna_dashboard/ts/components/GraphRank.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index 6781793f..a0ffd6ed 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -274,7 +274,7 @@ const getColors = (values: number[]): number[] => { } const getOrderWithSameOrderAveraging = (values: number[]): number[] => { - const sortedValues = values.slice().sort() + const sortedValues = values.slice().sort((a, b) => a - b) const ranks: number[] = [] values.forEach((value) => { const firstIndex = sortedValues.indexOf(value) From 1f29783877da2feab59f24808ce0017a5701046b Mon Sep 17 00:00:00 2001 From: contramundum53 Date: Thu, 30 Nov 2023 16:46:08 +0900 Subject: [PATCH 136/176] Update python_tests/artifact/test_backend.py Co-authored-by: c-bata --- python_tests/artifact/test_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_tests/artifact/test_backend.py b/python_tests/artifact/test_backend.py index 0bb662aa..35caad85 100644 --- a/python_tests/artifact/test_backend.py +++ b/python_tests/artifact/test_backend.py @@ -201,7 +201,7 @@ def test_upload_artifact_invalid() -> None: body=json.dumps({"file": DUMMY_DATA}), content_type="application/json", ) - assert status == 500 # TODO: This should return 400 + assert status == 500 # TODO(contramundum53): This should return 400 # Invalid: complete trial study.add_trial(optuna.create_trial(value=1.0, distributions={}, params={})) From 5bbb11cf4654116fef9d3669e046666d8a00ecf0 Mon Sep 17 00:00:00 2001 From: Contramundum Date: Mon, 4 Dec 2023 16:10:55 +0900 Subject: [PATCH 137/176] code fix --- python_tests/artifact/test_backend.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/python_tests/artifact/test_backend.py b/python_tests/artifact/test_backend.py index 0bb662aa..76627131 100644 --- a/python_tests/artifact/test_backend.py +++ b/python_tests/artifact/test_backend.py @@ -181,10 +181,10 @@ def test_successful_trial_artifact_retrieval() -> None: assert body == b"dummy_content" -DUMMY_DATA = f"data:text/plain; charset=utf-8,{base64.b64encode(b'dummy_content').decode('utf-8')}" +DUMMY_DATA_URL = f"data:text/plain; charset=utf-8,{base64.b64encode(b'dummy_content').decode('utf-8')}" -def test_upload_artifact_invalid() -> None: +def test_upload_artifact_invalid_no_trial() -> None: storage = optuna.storages.InMemoryStorage() with tempfile.TemporaryDirectory() as tmpdir: @@ -193,24 +193,31 @@ def test_upload_artifact_invalid() -> None: app = create_app(storage, artifact_store) study = optuna.create_study(storage=storage) - # Invalid: no trial status, _, body = send_request( app, f"/api/artifacts/{study._study_id}/0", "POST", - body=json.dumps({"file": DUMMY_DATA}), + body=json.dumps({"file": DUMMY_DATA_URL}), content_type="application/json", ) assert status == 500 # TODO: This should return 400 - # Invalid: complete trial +def test_upload_artifact_invalid_complete_trial() -> None: + storage = optuna.storages.InMemoryStorage() + + with tempfile.TemporaryDirectory() as tmpdir: + artifact_store = FileSystemArtifactStore(tmpdir) + + app = create_app(storage, artifact_store) + study = optuna.create_study(storage=storage) + study.add_trial(optuna.create_trial(value=1.0, distributions={}, params={})) trial = study.trials[-1] status, _, body = send_request( app, f"/api/artifacts/{study._study_id}/{trial._trial_id}", "POST", - body=json.dumps({"file": DUMMY_DATA}), + body=json.dumps({"file": DUMMY_DATA_URL}), content_type="application/json", ) assert status == 400 @@ -231,7 +238,7 @@ def test_upload_artifact() -> None: app, f"/api/artifacts/{study._study_id}/{trial._trial_id}", "POST", - body=json.dumps({"file": DUMMY_DATA}), + body=json.dumps({"file": DUMMY_DATA_URL}), content_type="application/json", ) assert status == 201 From 9e314a96ddb57a87cd3243fe987361a6a88402ed Mon Sep 17 00:00:00 2001 From: Contramundum Date: Mon, 4 Dec 2023 16:12:29 +0900 Subject: [PATCH 138/176] format --- python_tests/artifact/test_backend.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python_tests/artifact/test_backend.py b/python_tests/artifact/test_backend.py index f2192f03..52fe6027 100644 --- a/python_tests/artifact/test_backend.py +++ b/python_tests/artifact/test_backend.py @@ -181,7 +181,9 @@ def test_successful_trial_artifact_retrieval() -> None: assert body == b"dummy_content" -DUMMY_DATA_URL = f"data:text/plain; charset=utf-8,{base64.b64encode(b'dummy_content').decode('utf-8')}" +DUMMY_DATA_URL = ( + f"data:text/plain; charset=utf-8,{base64.b64encode(b'dummy_content').decode('utf-8')}" +) def test_upload_artifact_invalid_no_trial() -> None: @@ -202,6 +204,7 @@ def test_upload_artifact_invalid_no_trial() -> None: ) assert status == 500 # TODO(contramundum53): This should return 400 + def test_upload_artifact_invalid_complete_trial() -> None: storage = optuna.storages.InMemoryStorage() From 547d985a9fe3d1634928e34f0b89374a4fd681d1 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 11:37:58 +0900 Subject: [PATCH 139/176] Update _app.py --- optuna_dashboard/_app.py | 51 +++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index 8baa1280..d9d04666 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -2,7 +2,7 @@ from __future__ import annotations import csv import functools -import io +import io import logging import os import typing @@ -453,38 +453,57 @@ def create_app( @app.get("/csv/") def download_csv(study_id: int) -> BottleViewReturn: - # TODO: Create a CSV file + # Create a CSV file summary = get_study_summary(storage, study_id) if summary is None: response.status = 404 # Not found return {"reason": f"study_id={study_id} is not found"} trials = get_trials(storage, study_id) - param_names = list(trials[0].params.keys()) - union_user_attrs = list(trials[0].user_attrs) - column_names = ["Number", "State", "Value"] + param_names + union_user_attrs + param_names = [] + user_attr_names = [] + for trial in trials: + for param_name in trial.params.keys(): + if param_name not in param_names: + param_names.append(param_name) + for attr_name in trial.user_attrs.keys(): + if attr_name not in user_attr_names: + user_attr_names.append(attr_name) + + param_names_heading = [f"Param {x}" for x in param_names] + user_attr_names_heading = [f"UserAttribute {x}" for x in user_attr_names] + value_heading = ["Value"] + if len(trials[0].values) > 1: + value_heading = [f"Objective {x}" for x in range(len(trials[0].values))] + column_names = ( + ["Number", "State"] + value_heading + param_names_heading + user_attr_names_heading + ) buf = io.StringIO("") writer = csv.writer(buf) writer.writerow(column_names) for frozen_trial in trials: - row = [ - frozen_trial.number, - frozen_trial.state, - frozen_trial.values[0] - ] - row += [frozen_trial.params[param] for param in param_names] - row += [frozen_trial.user_attrs[attr] for attr in union_user_attrs] + row = [frozen_trial.number, frozen_trial.state.name] + row += frozen_trial.values + for param_name in param_names: + if param_name in frozen_trial.params.keys(): + row += [frozen_trial.params[param_name]] + else: + row += [None] + for attr_name in user_attr_names: + if attr_name in frozen_trial.user_attrs.keys(): + row += [frozen_trial.user_attrs[attr_name]] + else: + row += [None] writer.writerow(row) - - # TODO: Set response headers + + # Set response headers response.headers["Content-Type"] = "text/csv; chatset=cp932" response.headers["Content-Disposition"] = f"attachment; filename=trials_{study_id}.csv" - # TODO: Response body + # Response body buf.seek(0) return buf.read() - @app.get("/favicon.ico") def favicon() -> BottleViewReturn: From 65d7c8e4cc62818a4c304bad692b5c75652b05ef Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 11:39:12 +0900 Subject: [PATCH 140/176] add download button --- .../ts/components/StudyDetail.tsx | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/optuna_dashboard/ts/components/StudyDetail.tsx b/optuna_dashboard/ts/components/StudyDetail.tsx index f85d7458..ce7fe3ce 100644 --- a/optuna_dashboard/ts/components/StudyDetail.tsx +++ b/optuna_dashboard/ts/components/StudyDetail.tsx @@ -150,21 +150,39 @@ export const StudyDetail: FC<{ content = } else if (page === "trialTable") { content = ( - - - - - - - - + + + + + + + + Download CSV File + {" "} + + + + + + + + ) } else if (page === "note" && studyDetail !== null) { content = ( From 46ec27719566ae6c04462aec8e05d5b5aa9382e1 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 11:47:07 +0900 Subject: [PATCH 141/176] fix lint --- optuna_dashboard/_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index d9d04666..b2b13b67 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -450,7 +450,7 @@ def create_app( note.save_note_with_version(storage, study_id, trial_id, req_note_ver, req_note_body) response.status = 204 # No content return {} - + @app.get("/csv/") def download_csv(study_id: int) -> BottleViewReturn: # Create a CSV file From d18562f62c836308107a0bb3e8ba25b6277c9d48 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Wed, 6 Dec 2023 07:02:17 +0100 Subject: [PATCH 142/176] [bug] Fix an error caused by infinity in trial.values --- optuna_dashboard/ts/components/GraphRank.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index 7164b99b..cefe3145 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -152,13 +152,17 @@ const getRankPlotInfo = ( const zValues: number[] = [] const isFeasible: boolean[] = [] const hovertext: string[] = [] + const convertTrialValueToNumber = (value: TrialValueNumber): number => { + // TrialValueNumber takes `number`, "inf", or "-inf". + return typeof(value) === "number" ? value : value.includes("-") ? -Infinity : Infinity + }; filteredTrials.forEach((trial, i) => { const xValue = xAxis.values[i] const yValue = yAxis.values[i] if (xValue && yValue && trial.values) { xValues.push(xValue) yValues.push(yValue) - const zValue = Number(trial.values[objectiveId]) + const zValue = convertTrialValueToNumber(trial.values[objectiveId]) zValues.push(zValue) const feasibility = trial.constraints.every((c) => c <= 0) isFeasible.push(feasibility) From bdd27590a97a69216942d0d6e25b569fe5d3ba13 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Wed, 6 Dec 2023 07:12:07 +0100 Subject: [PATCH 143/176] Apply formatter --- optuna_dashboard/ts/components/GraphRank.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/ts/components/GraphRank.tsx b/optuna_dashboard/ts/components/GraphRank.tsx index cefe3145..6daaf10a 100644 --- a/optuna_dashboard/ts/components/GraphRank.tsx +++ b/optuna_dashboard/ts/components/GraphRank.tsx @@ -154,8 +154,12 @@ const getRankPlotInfo = ( const hovertext: string[] = [] const convertTrialValueToNumber = (value: TrialValueNumber): number => { // TrialValueNumber takes `number`, "inf", or "-inf". - return typeof(value) === "number" ? value : value.includes("-") ? -Infinity : Infinity - }; + return typeof value === "number" + ? value + : value.includes("-") + ? -Infinity + : Infinity + } filteredTrials.forEach((trial, i) => { const xValue = xAxis.values[i] const yValue = yAxis.values[i] From 6f681c40304c27b26066d513e645098284d95ddb Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 15:25:57 +0900 Subject: [PATCH 144/176] Update optuna_dashboard/_app.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- optuna_dashboard/_app.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index b2b13b67..c0476268 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -483,18 +483,9 @@ def create_app( writer = csv.writer(buf) writer.writerow(column_names) for frozen_trial in trials: - row = [frozen_trial.number, frozen_trial.state.name] - row += frozen_trial.values - for param_name in param_names: - if param_name in frozen_trial.params.keys(): - row += [frozen_trial.params[param_name]] - else: - row += [None] - for attr_name in user_attr_names: - if attr_name in frozen_trial.user_attrs.keys(): - row += [frozen_trial.user_attrs[attr_name]] - else: - row += [None] + row = [frozen_trial.number, frozen_trial.state.name] + frozen_trial.values + row.extend([frozen_trial.params.get(name, None) for name in param_names]) + row.extend([frozen_trial.user_attrs.get(name, None) for name in user_attr_names]) writer.writerow(row) # Set response headers From 3b3142209982034f0f7f38fc0db2e4bd2eebbb53 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 15:28:19 +0900 Subject: [PATCH 145/176] Update optuna_dashboard/_app.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- optuna_dashboard/_app.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index c0476268..db50e1f4 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -460,15 +460,8 @@ def create_app( return {"reason": f"study_id={study_id} is not found"} trials = get_trials(storage, study_id) - param_names = [] - user_attr_names = [] - for trial in trials: - for param_name in trial.params.keys(): - if param_name not in param_names: - param_names.append(param_name) - for attr_name in trial.user_attrs.keys(): - if attr_name not in user_attr_names: - user_attr_names.append(attr_name) + param_names = sorted(set(chain.from_iterable([t.params.keys() for t in trials]))) + user_attr_names = sorted(set(chain.from_iterable([t.user_attrs.keys() for t in trials]))) param_names_heading = [f"Param {x}" for x in param_names] user_attr_names_heading = [f"UserAttribute {x}" for x in user_attr_names] From a5502cefc21f3ac1333d8f642acb0a7687b11fe4 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 15:31:04 +0900 Subject: [PATCH 146/176] Update _app.py --- optuna_dashboard/_app.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index db50e1f4..0af6756d 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -3,6 +3,7 @@ from __future__ import annotations import csv import functools import io +from itertools import chain import logging import os import typing @@ -463,13 +464,13 @@ def create_app( param_names = sorted(set(chain.from_iterable([t.params.keys() for t in trials]))) user_attr_names = sorted(set(chain.from_iterable([t.user_attrs.keys() for t in trials]))) - param_names_heading = [f"Param {x}" for x in param_names] - user_attr_names_heading = [f"UserAttribute {x}" for x in user_attr_names] - value_heading = ["Value"] + param_names_header = [f"Param {x}" for x in param_names] + user_attr_names_header = [f"UserAttribute {x}" for x in user_attr_names] + value_header = ["Value"] if len(trials[0].values) > 1: - value_heading = [f"Objective {x}" for x in range(len(trials[0].values))] + value_header = [f"Objective {x}" for x in range(len(trials[0].values))] column_names = ( - ["Number", "State"] + value_heading + param_names_heading + user_attr_names_heading + ["Number", "State"] + value_header + param_names_header + user_attr_names_header ) buf = io.StringIO("") From c73abacef3c96569638b120d28a7965f220c79c6 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 15:35:35 +0900 Subject: [PATCH 147/176] Add files via upload --- python_tests/test_csv_download.py | 73 +++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 python_tests/test_csv_download.py diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py new file mode 100644 index 00000000..a820f37b --- /dev/null +++ b/python_tests/test_csv_download.py @@ -0,0 +1,73 @@ +import optuna +from optuna_dashboard._app import create_app +import pytest + +from .wsgi_client import send_request + + +@pytest.mark.parametrize("id", [0, 1]) +def test_download_csv_fail(id: int) -> None: + def objective(trial: optuna.Trial) -> float: + x = trial.suggest_float("x", -100, 100) + y = trial.suggest_categorical("y", [-1, 0, 1]) + return x**2 + y + + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + optuna.logging.set_verbosity(optuna.logging.ERROR) + study.optimize(objective, n_trials=10) + app = create_app(storage) + status, _, body = send_request( + app, + f"/csv/{id}", + "GET", + content_type="application/json", + ) + assert status == (404 if id != 0 else 200) + + +@pytest.mark.parametrize("is_multi_obj", [True, False]) +def test_download_csv_multi_obj(is_multi_obj: bool) -> None: + def objective(trial: optuna.Trial) -> float: + x = trial.suggest_float("x", -100, 100) + y = trial.suggest_categorical("y", [-1, 0, 1]) + if is_multi_obj: + return x**2, y + return x**2 + y + + storage = optuna.storages.InMemoryStorage() + if is_multi_obj: + study = optuna.create_study(storage=storage, directions=["minimize", "minimize"]) + else: + study = optuna.create_study(storage=storage) + optuna.logging.set_verbosity(optuna.logging.ERROR) + study.optimize(objective, n_trials=10) + app = create_app(storage) + status, _, body = send_request( + app, + f"/csv/{0}", + "GET", + content_type="application/json", + ) + assert status == 200 + + +def test_download_csv_user_attr() -> None: + def objective(trial: optuna.Trial) -> float: + x = trial.suggest_float("x", -100, 100) + y = trial.suggest_categorical("y", [-1, 0, 1]) + trial.set_user_attr("abs_y", abs(y)) + return x**2 + y + + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + optuna.logging.set_verbosity(optuna.logging.ERROR) + study.optimize(objective, n_trials=10) + app = create_app(storage) + status, _, body = send_request( + app, + f"/csv/{0}", + "GET", + content_type="application/json", + ) + assert status == 200 From 26d29f3c02e03b1b51127a642485a25da83e1fe9 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 15:54:40 +0900 Subject: [PATCH 148/176] fix mypy --- python_tests/test_csv_download.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py index a820f37b..de144fd0 100644 --- a/python_tests/test_csv_download.py +++ b/python_tests/test_csv_download.py @@ -1,3 +1,5 @@ +from typing import Any + import optuna from optuna_dashboard._app import create_app import pytest @@ -28,7 +30,7 @@ def test_download_csv_fail(id: int) -> None: @pytest.mark.parametrize("is_multi_obj", [True, False]) def test_download_csv_multi_obj(is_multi_obj: bool) -> None: - def objective(trial: optuna.Trial) -> float: + def objective(trial: optuna.Trial) -> Any: x = trial.suggest_float("x", -100, 100) y = trial.suggest_categorical("y", [-1, 0, 1]) if is_multi_obj: From 895fc0bd0489a3dc9693a5ecbcef7f192eeafad5 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 16:26:02 +0900 Subject: [PATCH 149/176] Update optuna_dashboard/_app.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- optuna_dashboard/_app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index 0af6756d..f6718945 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -460,6 +460,8 @@ def create_app( response.status = 404 # Not found return {"reason": f"study_id={study_id} is not found"} trials = get_trials(storage, study_id) + if len(trials) == 0: + return {"reason": f"study_id={study_id} has no trials"} param_names = sorted(set(chain.from_iterable([t.params.keys() for t in trials]))) user_attr_names = sorted(set(chain.from_iterable([t.user_attrs.keys() for t in trials]))) From 908788d029a6bc626eb4f1d69f6006c206160e9a Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 16:26:38 +0900 Subject: [PATCH 150/176] Update optuna_dashboard/_app.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- optuna_dashboard/_app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index f6718945..7f323518 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -469,8 +469,9 @@ def create_app( param_names_header = [f"Param {x}" for x in param_names] user_attr_names_header = [f"UserAttribute {x}" for x in user_attr_names] value_header = ["Value"] - if len(trials[0].values) > 1: - value_header = [f"Objective {x}" for x in range(len(trials[0].values))] + n_objs = max([len(t.values) for t in trials if t.values is not None], default=1) + if n_objs > 1: + value_header = [f"Objective {x}" for x in range(n_objs)] column_names = ( ["Number", "State"] + value_header + param_names_header + user_attr_names_header ) From 2a03cd52855d3ff16a59e29fe5aae74f1e28c095 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 16:26:55 +0900 Subject: [PATCH 151/176] Update optuna_dashboard/_app.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- optuna_dashboard/_app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index 7f323518..ad4934e4 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -480,7 +480,8 @@ def create_app( writer = csv.writer(buf) writer.writerow(column_names) for frozen_trial in trials: - row = [frozen_trial.number, frozen_trial.state.name] + frozen_trial.values + row = [frozen_trial.number, frozen_trial.state.name] + row.extend(frozen_trial.values if frozen_trial.values is not None else [None] * n_objs) row.extend([frozen_trial.params.get(name, None) for name in param_names]) row.extend([frozen_trial.user_attrs.get(name, None) for name in user_attr_names]) writer.writerow(row) From 8eac8fc6f31e7420f0f5a277ebdd17953c7602be Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 16:36:00 +0900 Subject: [PATCH 152/176] add tests --- python_tests/test_csv_download.py | 52 +++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py index de144fd0..b8491502 100644 --- a/python_tests/test_csv_download.py +++ b/python_tests/test_csv_download.py @@ -1,12 +1,60 @@ from typing import Any import optuna +from optuna.trial import TrialState from optuna_dashboard._app import create_app import pytest from .wsgi_client import send_request +def test_download_csv_no_trial() -> None: + def objective(trial: optuna.Trial) -> float: + x = trial.suggest_float("x", -100, 100) + y = trial.suggest_categorical("y", [-1, 0, 1]) + return x**2 + y + + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + study.optimize(objective, n_trials=0) + app = create_app(storage) + status, _, body = send_request( + app, + "/csv/0", + "GET", + content_type="application/json", + ) + assert status == 200 + + +def test_download_csv_all_waiting() -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + study.add_trial(optuna.trial.create_trial(state=TrialState.WAITING)) + app = create_app(storage) + status, _, body = send_request( + app, + "/csv/0", + "GET", + content_type="application/json", + ) + assert status == 200 + + +def test_download_csv_all_running() -> None: + storage = optuna.storages.InMemoryStorage() + study = optuna.create_study(storage=storage) + study.add_trial(optuna.trial.create_trial(state=TrialState.RUNNING)) + app = create_app(storage) + status, _, body = send_request( + app, + "/csv/0", + "GET", + content_type="application/json", + ) + assert status == 200 + + @pytest.mark.parametrize("id", [0, 1]) def test_download_csv_fail(id: int) -> None: def objective(trial: optuna.Trial) -> float: @@ -47,7 +95,7 @@ def test_download_csv_multi_obj(is_multi_obj: bool) -> None: app = create_app(storage) status, _, body = send_request( app, - f"/csv/{0}", + "/csv/0", "GET", content_type="application/json", ) @@ -68,7 +116,7 @@ def test_download_csv_user_attr() -> None: app = create_app(storage) status, _, body = send_request( app, - f"/csv/{0}", + "/csv/0", "GET", content_type="application/json", ) From 2fde74b00dca4b7ee7a62a1b73b220b2a1ab40f6 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 17:19:42 +0900 Subject: [PATCH 153/176] Update optuna_dashboard/_app.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- optuna_dashboard/_app.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index ad4934e4..8ddd9526 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -460,8 +460,6 @@ def create_app( response.status = 404 # Not found return {"reason": f"study_id={study_id} is not found"} trials = get_trials(storage, study_id) - if len(trials) == 0: - return {"reason": f"study_id={study_id} has no trials"} param_names = sorted(set(chain.from_iterable([t.params.keys() for t in trials]))) user_attr_names = sorted(set(chain.from_iterable([t.user_attrs.keys() for t in trials]))) From 3d6e69a16905f78b6b5221bb0e1dff05fe426b15 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 17:48:09 +0900 Subject: [PATCH 154/176] revised _app.py --- optuna_dashboard/_app.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index 8ddd9526..a74ce1e9 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -6,6 +6,7 @@ import io from itertools import chain import logging import os +import re import typing from typing import Any from typing import Optional @@ -467,9 +468,12 @@ def create_app( param_names_header = [f"Param {x}" for x in param_names] user_attr_names_header = [f"UserAttribute {x}" for x in user_attr_names] value_header = ["Value"] - n_objs = max([len(t.values) for t in trials if t.values is not None], default=1) + n_objs = len(summary.directions) if n_objs > 1: - value_header = [f"Objective {x}" for x in range(n_objs)] + if "study:metric_names" in summary._system_attrs: + value_header = summary._system_attrs["study:metric_names"] + else: + value_header = [f"Objective {x}" for x in range(n_objs)] column_names = ( ["Number", "State"] + value_header + param_names_header + user_attr_names_header ) @@ -485,8 +489,9 @@ def create_app( writer.writerow(row) # Set response headers + output_filename = re.sub(r'[\\/:*?"<>|]+', "", summary.study_name) response.headers["Content-Type"] = "text/csv; chatset=cp932" - response.headers["Content-Disposition"] = f"attachment; filename=trials_{study_id}.csv" + response.headers["Content-Disposition"] = f"attachment; filename=trials_{output_filename}.csv" # Response body buf.seek(0) From e5e813bdde012379fbec3dbc229ce87c31fa96bf Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 17:56:00 +0900 Subject: [PATCH 155/176] fix lint --- optuna_dashboard/_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index a74ce1e9..115d4078 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -489,9 +489,9 @@ def create_app( writer.writerow(row) # Set response headers - output_filename = re.sub(r'[\\/:*?"<>|]+', "", summary.study_name) + output_name = re.sub(r'[\\/:*?"<>|]+', "", summary.study_name) response.headers["Content-Type"] = "text/csv; chatset=cp932" - response.headers["Content-Disposition"] = f"attachment; filename=trials_{output_filename}.csv" + response.headers["Content-Disposition"] = f"attachment; filename={output_name}.csv" # Response body buf.seek(0) From f57bab2d7d8a8bc3b897958148717c4764e914aa Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 18:05:15 +0900 Subject: [PATCH 156/176] revised _app.py --- optuna_dashboard/_app.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index 115d4078..6a916c5d 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -456,24 +456,22 @@ def create_app( @app.get("/csv/") def download_csv(study_id: int) -> BottleViewReturn: # Create a CSV file - summary = get_study_summary(storage, study_id) - if summary is None: + try: + study_name = storage.get_study_name_from_id(study_id) + study = optuna.load_study(storage=storage, study_name=study_name) + except KeyError: response.status = 404 # Not found return {"reason": f"study_id={study_id} is not found"} - trials = get_trials(storage, study_id) - + trials = study.trials param_names = sorted(set(chain.from_iterable([t.params.keys() for t in trials]))) user_attr_names = sorted(set(chain.from_iterable([t.user_attrs.keys() for t in trials]))) - param_names_header = [f"Param {x}" for x in param_names] user_attr_names_header = [f"UserAttribute {x}" for x in user_attr_names] - value_header = ["Value"] - n_objs = len(summary.directions) - if n_objs > 1: - if "study:metric_names" in summary._system_attrs: - value_header = summary._system_attrs["study:metric_names"] - else: - value_header = [f"Objective {x}" for x in range(n_objs)] + n_objs = len(study.directions) + if study.metric_names is not None: + value_header = study.metric_names + else: + value_header = ["Value"] if n_objs == 1 else [f"Objective {x}" for x in range(n_objs)] column_names = ( ["Number", "State"] + value_header + param_names_header + user_attr_names_header ) @@ -489,7 +487,7 @@ def create_app( writer.writerow(row) # Set response headers - output_name = re.sub(r'[\\/:*?"<>|]+', "", summary.study_name) + output_name = re.sub(r'[\\/:*?"<>|]+', "", study_name) response.headers["Content-Type"] = "text/csv; chatset=cp932" response.headers["Content-Disposition"] = f"attachment; filename={output_name}.csv" From c9915ad12d3640741ffbc9e826689fdbe30d80f3 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 18:26:17 +0900 Subject: [PATCH 157/176] Update optuna_dashboard/_app.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- optuna_dashboard/_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index 6a916c5d..ead27188 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -487,7 +487,7 @@ def create_app( writer.writerow(row) # Set response headers - output_name = re.sub(r'[\\/:*?"<>|]+', "", study_name) + output_name = "-".join(re.sub(r'[\\/:*?"<>|]+', "", study_name).split(" ")) response.headers["Content-Type"] = "text/csv; chatset=cp932" response.headers["Content-Disposition"] = f"attachment; filename={output_name}.csv" From 7d834d3aaed6b4fc2078c52623b64f8986a9e94b Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Wed, 6 Dec 2023 18:36:56 +0900 Subject: [PATCH 158/176] Update StudyDetail.tsx --- .../ts/components/StudyDetail.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/optuna_dashboard/ts/components/StudyDetail.tsx b/optuna_dashboard/ts/components/StudyDetail.tsx index ce7fe3ce..2c464027 100644 --- a/optuna_dashboard/ts/components/StudyDetail.tsx +++ b/optuna_dashboard/ts/components/StudyDetail.tsx @@ -151,19 +151,11 @@ export const StudyDetail: FC<{ } else if (page === "trialTable") { content = ( - - - - - - - Download CSV File - {" "} + + Download CSV File + + + + + + ) } else if (page === "note" && studyDetail !== null) { From afe0299e4ffe6bdc68cddbbed17c76e2067a1368 Mon Sep 17 00:00:00 2001 From: Toshihiko Yanase Date: Wed, 6 Dec 2023 22:31:23 +0900 Subject: [PATCH 159/176] Do not observe options of wavesurfer. --- optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx b/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx index 65d87bfb..74177602 100644 --- a/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx +++ b/optuna_dashboard/ts/components/WaveSurferArtifactViewer.tsx @@ -28,7 +28,7 @@ const useWavesurfer = ( return () => { ws.destroy() } - }, [options, containerRef]) + }, [containerRef]) return wavesurfer } From 64dc46c0c2552159709dc3ac56ff49ac807f1008 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 7 Dec 2023 10:30:09 +0900 Subject: [PATCH 160/176] Update python_tests/test_csv_download.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- python_tests/test_csv_download.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py index b8491502..89f8978f 100644 --- a/python_tests/test_csv_download.py +++ b/python_tests/test_csv_download.py @@ -18,7 +18,7 @@ def test_download_csv_no_trial() -> None: study = optuna.create_study(storage=storage) study.optimize(objective, n_trials=0) app = create_app(storage) - status, _, body = send_request( + status, _, _ = send_request( app, "/csv/0", "GET", From 462bc99b339b49733852e7c8cd8e73f4df4ccb5f Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 7 Dec 2023 10:31:31 +0900 Subject: [PATCH 161/176] Update python_tests/test_csv_download.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- python_tests/test_csv_download.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py index 89f8978f..55cd4569 100644 --- a/python_tests/test_csv_download.py +++ b/python_tests/test_csv_download.py @@ -86,10 +86,8 @@ def test_download_csv_multi_obj(is_multi_obj: bool) -> None: return x**2 + y storage = optuna.storages.InMemoryStorage() - if is_multi_obj: - study = optuna.create_study(storage=storage, directions=["minimize", "minimize"]) - else: - study = optuna.create_study(storage=storage) + directions = ["minimize", "minimize"] if is_multi_obj else ["minimize"] + study = optuna.create_study(storage=storage, directions=directions) optuna.logging.set_verbosity(optuna.logging.ERROR) study.optimize(objective, n_trials=10) app = create_app(storage) From 678b15e60cc17ce5164e80610514fd60d1f3a477 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 7 Dec 2023 10:31:55 +0900 Subject: [PATCH 162/176] Update python_tests/test_csv_download.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- python_tests/test_csv_download.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py index 55cd4569..7740ea83 100644 --- a/python_tests/test_csv_download.py +++ b/python_tests/test_csv_download.py @@ -55,8 +55,8 @@ def test_download_csv_all_running() -> None: assert status == 200 -@pytest.mark.parametrize("id", [0, 1]) -def test_download_csv_fail(id: int) -> None: +@pytest.mark.parametrize("study_id", [0, 1]) +def test_download_csv_fail(study_id: int) -> None: def objective(trial: optuna.Trial) -> float: x = trial.suggest_float("x", -100, 100) y = trial.suggest_categorical("y", [-1, 0, 1]) From 18d68ea91959563d0364dc561ca4ee3576a1625a Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 7 Dec 2023 10:58:16 +0900 Subject: [PATCH 163/176] Update test_csv_download.py --- python_tests/test_csv_download.py | 69 ++++++++++--------------------- 1 file changed, 21 insertions(+), 48 deletions(-) diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py index 7740ea83..e1ab12c9 100644 --- a/python_tests/test_csv_download.py +++ b/python_tests/test_csv_download.py @@ -8,6 +8,21 @@ import pytest from .wsgi_client import send_request +def _validate_output( + storage: optuna.storages.BaseStorage, + correct_status: int, + study_id: int, +) -> None: + app = create_app(storage) + status, _, _ = send_request( + app, + f"/csv/{study_id}", + "GET", + content_type="application/json", + ) + assert status == correct_status + + def test_download_csv_no_trial() -> None: def objective(trial: optuna.Trial) -> float: x = trial.suggest_float("x", -100, 100) @@ -17,42 +32,21 @@ def test_download_csv_no_trial() -> None: storage = optuna.storages.InMemoryStorage() study = optuna.create_study(storage=storage) study.optimize(objective, n_trials=0) - app = create_app(storage) - status, _, _ = send_request( - app, - "/csv/0", - "GET", - content_type="application/json", - ) - assert status == 200 + _validate_output(storage, 200, 0) def test_download_csv_all_waiting() -> None: storage = optuna.storages.InMemoryStorage() study = optuna.create_study(storage=storage) study.add_trial(optuna.trial.create_trial(state=TrialState.WAITING)) - app = create_app(storage) - status, _, body = send_request( - app, - "/csv/0", - "GET", - content_type="application/json", - ) - assert status == 200 + _validate_output(storage, 200, 0) def test_download_csv_all_running() -> None: storage = optuna.storages.InMemoryStorage() study = optuna.create_study(storage=storage) study.add_trial(optuna.trial.create_trial(state=TrialState.RUNNING)) - app = create_app(storage) - status, _, body = send_request( - app, - "/csv/0", - "GET", - content_type="application/json", - ) - assert status == 200 + _validate_output(storage, 200, 0) @pytest.mark.parametrize("study_id", [0, 1]) @@ -66,14 +60,7 @@ def test_download_csv_fail(study_id: int) -> None: study = optuna.create_study(storage=storage) optuna.logging.set_verbosity(optuna.logging.ERROR) study.optimize(objective, n_trials=10) - app = create_app(storage) - status, _, body = send_request( - app, - f"/csv/{id}", - "GET", - content_type="application/json", - ) - assert status == (404 if id != 0 else 200) + _validate_output(storage, 404 if study_id != 0 else 200, study_id) @pytest.mark.parametrize("is_multi_obj", [True, False]) @@ -90,14 +77,7 @@ def test_download_csv_multi_obj(is_multi_obj: bool) -> None: study = optuna.create_study(storage=storage, directions=directions) optuna.logging.set_verbosity(optuna.logging.ERROR) study.optimize(objective, n_trials=10) - app = create_app(storage) - status, _, body = send_request( - app, - "/csv/0", - "GET", - content_type="application/json", - ) - assert status == 200 + _validate_output(storage, 200, 0) def test_download_csv_user_attr() -> None: @@ -111,11 +91,4 @@ def test_download_csv_user_attr() -> None: study = optuna.create_study(storage=storage) optuna.logging.set_verbosity(optuna.logging.ERROR) study.optimize(objective, n_trials=10) - app = create_app(storage) - status, _, body = send_request( - app, - "/csv/0", - "GET", - content_type="application/json", - ) - assert status == 200 + _validate_output(storage, 200, 0) From 4a3849b07312d53f8205b8e18fb9a442971108b9 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 7 Dec 2023 11:57:52 +0900 Subject: [PATCH 164/176] Update StudyDetail.tsx --- optuna_dashboard/ts/components/StudyDetail.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/optuna_dashboard/ts/components/StudyDetail.tsx b/optuna_dashboard/ts/components/StudyDetail.tsx index 2c464027..f7a6b0eb 100644 --- a/optuna_dashboard/ts/components/StudyDetail.tsx +++ b/optuna_dashboard/ts/components/StudyDetail.tsx @@ -157,8 +157,8 @@ export const StudyDetail: FC<{ width: "auto", height: "auto", display: "flex", - justifyContent: "center", - alignItems: "center", + justifyContent: "left", + alignItems: "left", }} > @@ -166,7 +166,6 @@ export const StudyDetail: FC<{ aria-label="download csv" size="small" color="inherit" - download={`trials_${studyDetail?.id}.csv`} sx={{ margin: "auto 0" }} href={`/csv/${studyDetail?.id}`} > From 6d442254e2019f85b38d9728de84134458b43216 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 7 Dec 2023 12:06:38 +0900 Subject: [PATCH 165/176] Update StudyDetail.tsx --- optuna_dashboard/ts/components/StudyDetail.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/optuna_dashboard/ts/components/StudyDetail.tsx b/optuna_dashboard/ts/components/StudyDetail.tsx index f7a6b0eb..79702f2e 100644 --- a/optuna_dashboard/ts/components/StudyDetail.tsx +++ b/optuna_dashboard/ts/components/StudyDetail.tsx @@ -166,6 +166,7 @@ export const StudyDetail: FC<{ aria-label="download csv" size="small" color="inherit" + download sx={{ margin: "auto 0" }} href={`/csv/${studyDetail?.id}`} > From 885da8fcacedf09ecd4477bbeb1451c183591277 Mon Sep 17 00:00:00 2001 From: Naoto Mizuno Date: Thu, 7 Dec 2023 13:27:38 +0900 Subject: [PATCH 166/176] Prevent layout shift with intermediate values plot by rearranging the order of graphs --- .../ts/components/StudyHistory.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/optuna_dashboard/ts/components/StudyHistory.tsx b/optuna_dashboard/ts/components/StudyHistory.tsx index 4bcbbc0a..8fab513a 100644 --- a/optuna_dashboard/ts/components/StudyHistory.tsx +++ b/optuna_dashboard/ts/components/StudyHistory.tsx @@ -108,17 +108,6 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { - {studyDetail !== null && - studyDetail.directions.length === 1 && - studyDetail.has_intermediate_values ? ( - - - - ) : null} = ({ studyId }) => { + {studyDetail !== null && + studyDetail.directions.length === 1 && + studyDetail.has_intermediate_values ? ( + + + + ) : null} {artifactEnabled && studyDetail !== null && ( From c0df7dc3d8ea792fb9b20f751e26a9206d21ef68 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Thu, 7 Dec 2023 07:46:52 +0100 Subject: [PATCH 167/176] Clear cache before going to another tests in unit tests --- e2e_tests/test_dashboard/visual_regression_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/e2e_tests/test_dashboard/visual_regression_test.py b/e2e_tests/test_dashboard/visual_regression_test.py index ea6ff930..876c585b 100644 --- a/e2e_tests/test_dashboard/visual_regression_test.py +++ b/e2e_tests/test_dashboard/visual_regression_test.py @@ -4,11 +4,22 @@ import optuna from playwright.sync_api import Page import pytest +from optuna_dashboard._storage import trials_cache +from optuna_dashboard._storage import trials_cache_lock +from optuna_dashboard._storage import trials_last_fetched_at + from ..test_server import make_test_server +def clear_inmemory_cache() -> None: + with trials_cache_lock: + trials_cache.clear() + trials_last_fetched_at.clear() + + @pytest.fixture def storage() -> optuna.storages.InMemoryStorage: + clear_inmemory_cache() storage = optuna.storages.InMemoryStorage() return storage From a5e2be25b54390dda882532c3cd227b0b04f74c3 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Thu, 7 Dec 2023 07:51:09 +0100 Subject: [PATCH 168/176] Apply isort --- e2e_tests/test_dashboard/visual_regression_test.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/e2e_tests/test_dashboard/visual_regression_test.py b/e2e_tests/test_dashboard/visual_regression_test.py index 876c585b..e5c3a992 100644 --- a/e2e_tests/test_dashboard/visual_regression_test.py +++ b/e2e_tests/test_dashboard/visual_regression_test.py @@ -1,12 +1,11 @@ from typing import Callable import optuna -from playwright.sync_api import Page -import pytest - from optuna_dashboard._storage import trials_cache from optuna_dashboard._storage import trials_cache_lock from optuna_dashboard._storage import trials_last_fetched_at +from playwright.sync_api import Page +import pytest from ..test_server import make_test_server From aa0fccc65ea68bdb5121312baa773d8949fbc791 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Thu, 7 Dec 2023 08:55:15 +0100 Subject: [PATCH 169/176] Clear cache for other tests as well --- .../test_usecases/test_preferential_optimization.py | 2 ++ .../test_dashboard/test_usecases/test_study_history.py | 2 ++ e2e_tests/test_dashboard/visual_regression_test.py | 10 +--------- e2e_tests/utils.py | 9 +++++++++ 4 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 e2e_tests/utils.py diff --git a/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py b/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py index fa3317dc..0b19fea0 100644 --- a/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py +++ b/e2e_tests/test_dashboard/test_usecases/test_preferential_optimization.py @@ -9,6 +9,7 @@ from playwright.sync_api import Page import pytest from ...test_server import make_test_server +from ...utils import clear_inmemory_cache def make_test_storage() -> optuna.storages.InMemoryStorage: @@ -43,6 +44,7 @@ def make_test_storage() -> optuna.storages.InMemoryStorage: @pytest.fixture def storage() -> optuna.storages.InMemoryStorage: + clear_inmemory_cache() storage = make_test_storage() return storage diff --git a/e2e_tests/test_dashboard/test_usecases/test_study_history.py b/e2e_tests/test_dashboard/test_usecases/test_study_history.py index 6c22b16b..f2d937bb 100644 --- a/e2e_tests/test_dashboard/test_usecases/test_study_history.py +++ b/e2e_tests/test_dashboard/test_usecases/test_study_history.py @@ -3,6 +3,7 @@ from playwright.sync_api import Page import pytest from ...test_server import make_test_server +from ...utils import clear_inmemory_cache def make_test_storage() -> optuna.storages.InMemoryStorage: @@ -23,6 +24,7 @@ def make_test_storage() -> optuna.storages.InMemoryStorage: @pytest.fixture def storage() -> optuna.storages.InMemoryStorage: + clear_inmemory_cache() storage = make_test_storage() return storage diff --git a/e2e_tests/test_dashboard/visual_regression_test.py b/e2e_tests/test_dashboard/visual_regression_test.py index e5c3a992..ab189343 100644 --- a/e2e_tests/test_dashboard/visual_regression_test.py +++ b/e2e_tests/test_dashboard/visual_regression_test.py @@ -1,19 +1,11 @@ from typing import Callable import optuna -from optuna_dashboard._storage import trials_cache -from optuna_dashboard._storage import trials_cache_lock -from optuna_dashboard._storage import trials_last_fetched_at from playwright.sync_api import Page import pytest from ..test_server import make_test_server - - -def clear_inmemory_cache() -> None: - with trials_cache_lock: - trials_cache.clear() - trials_last_fetched_at.clear() +from ..utils import clear_inmemory_cache @pytest.fixture diff --git a/e2e_tests/utils.py b/e2e_tests/utils.py new file mode 100644 index 00000000..68952ae9 --- /dev/null +++ b/e2e_tests/utils.py @@ -0,0 +1,9 @@ +from optuna_dashboard._storage import trials_cache +from optuna_dashboard._storage import trials_cache_lock +from optuna_dashboard._storage import trials_last_fetched_at + + +def clear_inmemory_cache() -> None: + with trials_cache_lock: + trials_cache.clear() + trials_last_fetched_at.clear() From e1f43e1ce4e139df05bf48b282d09ddffa060955 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 7 Dec 2023 18:14:24 +0900 Subject: [PATCH 170/176] Update python_tests/test_csv_download.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- python_tests/test_csv_download.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py index e1ab12c9..bb4c2bf8 100644 --- a/python_tests/test_csv_download.py +++ b/python_tests/test_csv_download.py @@ -1,5 +1,8 @@ +from __future__ import annotations + from typing import Any + import optuna from optuna.trial import TrialState from optuna_dashboard._app import create_app From a4fb8fdcdbf33393da21cf75ab2be9c0dd344e81 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 7 Dec 2023 18:14:55 +0900 Subject: [PATCH 171/176] Update python_tests/test_csv_download.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- python_tests/test_csv_download.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py index bb4c2bf8..3c615995 100644 --- a/python_tests/test_csv_download.py +++ b/python_tests/test_csv_download.py @@ -15,15 +15,23 @@ def _validate_output( storage: optuna.storages.BaseStorage, correct_status: int, study_id: int, + expect_no_result: bool = False, + extra_col_names: list[str] | None = None, ) -> None: app = create_app(storage) - status, _, _ = send_request( + status, _, body = send_request( app, f"/csv/{study_id}", "GET", content_type="application/json", ) assert status == correct_status + decoded_csv = str(body.decode("utf-8")) + if expect_no_result: + assert "is not found" in decoded_csv + else: + col_names = ["Number", "State"] + ([] if extra_col_names is None else extra_col_names) + assert all(col_name in decoded_csv for col_name in col_names) def test_download_csv_no_trial() -> None: From 1e5d582572b29b4fa85ed239a258de15df079581 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 7 Dec 2023 18:15:06 +0900 Subject: [PATCH 172/176] Update python_tests/test_csv_download.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- python_tests/test_csv_download.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py index 3c615995..a93789e7 100644 --- a/python_tests/test_csv_download.py +++ b/python_tests/test_csv_download.py @@ -71,7 +71,9 @@ def test_download_csv_fail(study_id: int) -> None: study = optuna.create_study(storage=storage) optuna.logging.set_verbosity(optuna.logging.ERROR) study.optimize(objective, n_trials=10) - _validate_output(storage, 404 if study_id != 0 else 200, study_id) + expect_no_result = study_id != 0 + cols = ["Param x", "Param y", "Value"] + _validate_output(storage, 404 if expect_no_result else 200, study_id, expect_no_result, cols) @pytest.mark.parametrize("is_multi_obj", [True, False]) From d7ff6f41f45fb1d8dbc5e61ff3642d7c99f52f5f Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 7 Dec 2023 18:15:19 +0900 Subject: [PATCH 173/176] Update python_tests/test_csv_download.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- python_tests/test_csv_download.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py index a93789e7..3eb0f330 100644 --- a/python_tests/test_csv_download.py +++ b/python_tests/test_csv_download.py @@ -90,7 +90,10 @@ def test_download_csv_multi_obj(is_multi_obj: bool) -> None: study = optuna.create_study(storage=storage, directions=directions) optuna.logging.set_verbosity(optuna.logging.ERROR) study.optimize(objective, n_trials=10) - _validate_output(storage, 200, 0) + cols = ["Param x", "Param y"] + cols += ["Objective 0", "Objective 1"] if is_multi_obj else ["Value"] + _validate_output(storage, 200, 0, extra_col_names=cols) + def test_download_csv_user_attr() -> None: From a8c451ada283cecf18aa8f8a036f7ea1816d5999 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 7 Dec 2023 18:15:30 +0900 Subject: [PATCH 174/176] Update python_tests/test_csv_download.py Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- python_tests/test_csv_download.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py index 3eb0f330..f43c8751 100644 --- a/python_tests/test_csv_download.py +++ b/python_tests/test_csv_download.py @@ -107,4 +107,6 @@ def test_download_csv_user_attr() -> None: study = optuna.create_study(storage=storage) optuna.logging.set_verbosity(optuna.logging.ERROR) study.optimize(objective, n_trials=10) - _validate_output(storage, 200, 0) + cols = ["Param x", "Param y", "Value", "UserAttribute abs_y"] + _validate_output(storage, 200, 0, extra_col_names=cols) + From f63b5e9cedd733df22cd6cab3da3731a4ffcbf58 Mon Sep 17 00:00:00 2001 From: Hiroki Takizawa Date: Thu, 7 Dec 2023 18:21:46 +0900 Subject: [PATCH 175/176] fix lint --- python_tests/test_csv_download.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/python_tests/test_csv_download.py b/python_tests/test_csv_download.py index f43c8751..019f18ba 100644 --- a/python_tests/test_csv_download.py +++ b/python_tests/test_csv_download.py @@ -2,7 +2,6 @@ from __future__ import annotations from typing import Any - import optuna from optuna.trial import TrialState from optuna_dashboard._app import create_app @@ -95,7 +94,6 @@ def test_download_csv_multi_obj(is_multi_obj: bool) -> None: _validate_output(storage, 200, 0, extra_col_names=cols) - def test_download_csv_user_attr() -> None: def objective(trial: optuna.Trial) -> float: x = trial.suggest_float("x", -100, 100) @@ -109,4 +107,3 @@ def test_download_csv_user_attr() -> None: study.optimize(objective, n_trials=10) cols = ["Param x", "Param y", "Value", "UserAttribute abs_y"] _validate_output(storage, 200, 0, extra_col_names=cols) - From c38d0d9f81449ea659da8f892933e67eb9d14a44 Mon Sep 17 00:00:00 2001 From: c-bata Date: Fri, 8 Dec 2023 14:16:49 +0900 Subject: [PATCH 176/176] Bump the version up to v0.14.0 --- optuna_dashboard/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optuna_dashboard/__init__.py b/optuna_dashboard/__init__.py index 8b793e62..2ca1d487 100644 --- a/optuna_dashboard/__init__.py +++ b/optuna_dashboard/__init__.py @@ -17,4 +17,4 @@ from ._note import save_note # noqa from ._preference_setting import register_preference_feedback_component # noqa -__version__ = "0.13.0" +__version__ = "0.14.0"