mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-24 13:41:07 +08:00
Merge pull request #329 from c-bata/beta-ui
Make a lot of improvements in the new Dashboard UI
This commit is contained in:
+32
-10
@@ -336,27 +336,26 @@ def create_app(storage: BaseStorage, debug: bool = False) -> Bottle:
|
||||
@app.get("/api/studies/<study_id:int>/param_importances")
|
||||
@json_api_view
|
||||
def get_param_importances(study_id: int) -> BottleViewReturn:
|
||||
# TODO(chenghuzi): add support for selecting params via query parameters.
|
||||
objective_id = int(request.params.get("objective_id", 0))
|
||||
try:
|
||||
n_directions = len(storage.get_study_directions(study_id))
|
||||
except KeyError:
|
||||
response.status = 404 # Study is not found
|
||||
return {"reason": f"study_id={study_id} is not found"}
|
||||
if objective_id >= n_directions:
|
||||
response.status = 400 # Bad request
|
||||
return {"reason": f"study_id={study_id} has only {n_directions} direction(s)."}
|
||||
|
||||
trials = get_trials(storage, study_id)
|
||||
try:
|
||||
return get_param_importance_from_trials_cache(storage, study_id, objective_id, trials)
|
||||
importances = [
|
||||
get_param_importance_from_trials_cache(storage, study_id, objective_id, trials)
|
||||
for objective_id in range(n_directions)
|
||||
]
|
||||
return {"param_importances": importances}
|
||||
except ValueError as e:
|
||||
response.status = 400 # Bad request
|
||||
return {"reason": str(e)}
|
||||
|
||||
@app.put("/api/studies/<study_id:int>/note")
|
||||
@json_api_view
|
||||
def save_note(study_id: int) -> BottleViewReturn:
|
||||
def save_study_note(study_id: int) -> BottleViewReturn:
|
||||
req_note_ver = request.json.get("version", None)
|
||||
req_note_body = request.json.get("body", None)
|
||||
if req_note_ver is None or req_note_body is None:
|
||||
@@ -364,15 +363,38 @@ def create_app(storage: BaseStorage, debug: bool = False) -> Bottle:
|
||||
return {"reason": "Invalid request."}
|
||||
|
||||
system_attrs = storage.get_study_system_attrs(study_id)
|
||||
if not note.version_is_incremented(system_attrs, req_note_ver):
|
||||
if not note.version_is_incremented(system_attrs, None, req_note_ver):
|
||||
response.status = 409 # Conflict
|
||||
return {
|
||||
"reason": "The text you are editing has changed. "
|
||||
"Please copy your edits and refresh the page.",
|
||||
"note": note.get_note_from_system_attrs(system_attrs),
|
||||
"note": note.get_note_from_system_attrs(system_attrs, None),
|
||||
}
|
||||
|
||||
note.save_note(storage, study_id, req_note_ver, req_note_body)
|
||||
note.save_note(storage, study_id, None, req_note_ver, req_note_body)
|
||||
response.status = 204 # No content
|
||||
return {}
|
||||
|
||||
@app.put("/api/studies/<study_id:int>/<trial_id:int>/note")
|
||||
@json_api_view
|
||||
def save_trial_note(study_id: int, trial_id: int) -> BottleViewReturn:
|
||||
req_note_ver = request.json.get("version", None)
|
||||
req_note_body = request.json.get("body", None)
|
||||
if req_note_ver is None or req_note_body is None:
|
||||
response.status = 400 # Bad request
|
||||
return {"reason": "Invalid request."}
|
||||
|
||||
# Store note content in study system attrs since it's always updatable.
|
||||
system_attrs = storage.get_study_system_attrs(study_id=study_id)
|
||||
if not note.version_is_incremented(system_attrs, trial_id, req_note_ver):
|
||||
response.status = 409 # Conflict
|
||||
return {
|
||||
"reason": "The text you are editing has changed. "
|
||||
"Please copy your edits and refresh the page.",
|
||||
"note": note.get_note_from_system_attrs(system_attrs, trial_id),
|
||||
}
|
||||
|
||||
note.save_note(storage, study_id, trial_id, req_note_ver, req_note_body)
|
||||
response.status = 204 # No content
|
||||
return {}
|
||||
|
||||
|
||||
@@ -25,26 +25,18 @@ except Exception as e:
|
||||
if TYPE_CHECKING:
|
||||
from typing import TypedDict
|
||||
|
||||
ImportanceItemType = TypedDict(
|
||||
"ImportanceItemType",
|
||||
ImportanceType = TypedDict(
|
||||
"ImportanceType",
|
||||
{
|
||||
"name": str,
|
||||
"importance": float,
|
||||
"distribution": str,
|
||||
},
|
||||
)
|
||||
ImportanceType = TypedDict(
|
||||
"ImportanceType",
|
||||
{
|
||||
"target_name": str,
|
||||
"param_importances": list[ImportanceItemType],
|
||||
},
|
||||
)
|
||||
|
||||
target_name = "Objective Value"
|
||||
param_importance_cache_lock = threading.Lock()
|
||||
# { "{study_id}:{objective_id}" : (n_completed_trials, importance) }
|
||||
param_importance_cache: dict[str, tuple[int, ImportanceType]] = {}
|
||||
param_importance_cache: dict[str, tuple[int, list[ImportanceType]]] = {}
|
||||
|
||||
|
||||
class StudyWrapper(Study):
|
||||
@@ -62,17 +54,15 @@ class StudyWrapper(Study):
|
||||
|
||||
def get_param_importance_from_trials_cache(
|
||||
storage: BaseStorage, study_id: int, objective_id: int, trials: list[FrozenTrial]
|
||||
) -> ImportanceType:
|
||||
) -> list[ImportanceType]:
|
||||
completed_trials = [t for t in trials if t.state == TrialState.COMPLETE]
|
||||
n_completed_trials = len(completed_trials)
|
||||
if n_completed_trials == 0:
|
||||
return {"target_name": target_name, "param_importances": []}
|
||||
return []
|
||||
|
||||
cache_key = f"{study_id}:{objective_id}"
|
||||
with param_importance_cache_lock:
|
||||
cache_n_trial, cache_importance = param_importance_cache.get(
|
||||
cache_key, (0, {"target_name": target_name, "param_importances": []})
|
||||
)
|
||||
cache_n_trial, cache_importance = param_importance_cache.get(cache_key, (0, []))
|
||||
if n_completed_trials == cache_n_trial:
|
||||
return cache_importance
|
||||
|
||||
@@ -95,18 +85,15 @@ def get_param_importance_from_trials_cache(
|
||||
|
||||
def convert_to_importance_type(
|
||||
importance: dict[str, float], trials: list[FrozenTrial]
|
||||
) -> ImportanceType:
|
||||
return {
|
||||
"target_name": target_name,
|
||||
"param_importances": [
|
||||
{
|
||||
"name": name,
|
||||
"importance": importance,
|
||||
"distribution": get_distribution_name(name, trials),
|
||||
}
|
||||
for name, importance in importance.items()
|
||||
],
|
||||
}
|
||||
) -> list[ImportanceType]:
|
||||
return [
|
||||
{
|
||||
"name": name,
|
||||
"importance": importance,
|
||||
"distribution": get_distribution_name(name, trials),
|
||||
}
|
||||
for name, importance in importance.items()
|
||||
]
|
||||
|
||||
|
||||
def get_distribution_name(param_name: str, trials: list[FrozenTrial]) -> str:
|
||||
|
||||
+39
-18
@@ -8,6 +8,7 @@ from optuna.storages import BaseStorage
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional
|
||||
from typing import TypedDict
|
||||
|
||||
NoteType = TypedDict(
|
||||
@@ -19,32 +20,50 @@ if TYPE_CHECKING:
|
||||
)
|
||||
|
||||
SYSTEM_ATTR_MAX_LENGTH = 2045
|
||||
NOTE_VER_KEY = "dashboard:note_ver"
|
||||
NOTE_STR_KEY_PREFIX = "dashboard:note_str:"
|
||||
|
||||
|
||||
def get_note_from_system_attrs(system_attrs: dict[str, Any]) -> NoteType:
|
||||
if NOTE_VER_KEY not in system_attrs:
|
||||
def note_ver_key(trial_id: Optional[int]) -> str:
|
||||
prefix = "dashboard:note_ver"
|
||||
if trial_id is None:
|
||||
return prefix
|
||||
return f"dashboard:{trial_id}:note_ver"
|
||||
|
||||
|
||||
def note_str_key_prefix(trial_id: Optional[int]) -> str:
|
||||
prefix = "dashboard:note_str:"
|
||||
if trial_id is None:
|
||||
return prefix
|
||||
return f"dashboard:{trial_id}:note_str:"
|
||||
|
||||
|
||||
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 {
|
||||
"version": 0,
|
||||
"body": "",
|
||||
}
|
||||
note_ver = int(system_attrs[NOTE_VER_KEY])
|
||||
note_ver = int(system_attrs[note_ver_key(trial_id)])
|
||||
note_attrs: dict[str, str] = {
|
||||
key: value for key, value in system_attrs.items() if key.startswith(NOTE_STR_KEY_PREFIX)
|
||||
key: value
|
||||
for key, value in system_attrs.items()
|
||||
if key.startswith(note_str_key_prefix(trial_id))
|
||||
}
|
||||
return {"version": note_ver, "body": concat_body(note_attrs)}
|
||||
return {"version": note_ver, "body": concat_body(note_attrs, trial_id)}
|
||||
|
||||
|
||||
def version_is_incremented(system_attrs: dict[str, Any], req_note_ver: int) -> bool:
|
||||
db_note_ver = system_attrs.get(NOTE_VER_KEY, 0)
|
||||
def version_is_incremented(
|
||||
system_attrs: dict[str, Any], trial_id: Optional[int], req_note_ver: int
|
||||
) -> bool:
|
||||
db_note_ver = system_attrs.get(note_ver_key(trial_id), 0)
|
||||
return req_note_ver == db_note_ver + 1
|
||||
|
||||
|
||||
def save_note(storage: BaseStorage, study_id: int, ver: int, body: str) -> None:
|
||||
storage.set_study_system_attr(study_id, NOTE_VER_KEY, ver)
|
||||
def save_note(
|
||||
storage: BaseStorage, study_id: int, trial_id: Optional[int], ver: int, body: str
|
||||
) -> None:
|
||||
storage.set_study_system_attr(study_id, note_ver_key(trial_id), ver)
|
||||
|
||||
attrs = split_body(body)
|
||||
attrs = split_body(body, trial_id)
|
||||
for k, v in attrs.items():
|
||||
storage.set_study_system_attr(study_id, k, v)
|
||||
|
||||
@@ -52,22 +71,24 @@ def save_note(storage: BaseStorage, study_id: int, ver: int, body: str) -> None:
|
||||
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)
|
||||
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}{i}", "")
|
||||
storage.set_study_system_attr(study_id, f"{note_str_key_prefix(trial_id)}{i}", "")
|
||||
|
||||
|
||||
def split_body(note_str: str) -> dict[str, str]:
|
||||
def split_body(note_str: str, trial_id: Optional[int]) -> dict[str, str]:
|
||||
note_len = len(note_str)
|
||||
attrs = {}
|
||||
for i in range(math.ceil(note_len / SYSTEM_ATTR_MAX_LENGTH)):
|
||||
start = i * SYSTEM_ATTR_MAX_LENGTH
|
||||
end = min((i + 1) * SYSTEM_ATTR_MAX_LENGTH, note_len)
|
||||
attrs[f"{NOTE_STR_KEY_PREFIX}{i}"] = note_str[start:end]
|
||||
attrs[f"{note_str_key_prefix(trial_id)}{i}"] = note_str[start:end]
|
||||
return attrs
|
||||
|
||||
|
||||
def concat_body(note_attrs: dict[str, str]) -> str:
|
||||
return "".join(note_attrs[f"{NOTE_STR_KEY_PREFIX}{i}"] for i in range(len(note_attrs)))
|
||||
def concat_body(note_attrs: dict[str, str], trial_id: Optional[int]) -> str:
|
||||
return "".join(
|
||||
note_attrs[f"{note_str_key_prefix(trial_id)}{i}"] for i in range(len(note_attrs))
|
||||
)
|
||||
|
||||
@@ -84,22 +84,27 @@ def serialize_study_detail(
|
||||
"name": summary.study_name,
|
||||
"directions": [d.name.lower() for d in summary.directions],
|
||||
}
|
||||
system_attrs = getattr(summary, "system_attrs", {})
|
||||
if summary.datetime_start is not None:
|
||||
serialized["datetime_start"] = summary.datetime_start.isoformat()
|
||||
|
||||
serialized["trials"] = [serialize_frozen_trial(summary._study_id, trial) for trial in trials]
|
||||
serialized["trials"] = [
|
||||
serialize_frozen_trial(summary._study_id, trial, system_attrs) for trial in trials
|
||||
]
|
||||
serialized["best_trials"] = [
|
||||
serialize_frozen_trial(summary._study_id, trial) for trial in best_trials
|
||||
serialize_frozen_trial(summary._study_id, trial, system_attrs) for trial in best_trials
|
||||
]
|
||||
serialized["intersection_search_space"] = serialize_search_space(intersection)
|
||||
serialized["union_search_space"] = serialize_search_space(union)
|
||||
serialized["union_user_attrs"] = [{"key": a[0], "sortable": a[1]} for a in union_user_attrs]
|
||||
serialized["has_intermediate_values"] = has_intermediate_values
|
||||
serialized["note"] = note.get_note_from_system_attrs(getattr(summary, "system_attrs", {}))
|
||||
serialized["note"] = note.get_note_from_system_attrs(system_attrs, None)
|
||||
return serialized
|
||||
|
||||
|
||||
def serialize_frozen_trial(study_id: int, trial: FrozenTrial) -> dict[str, Any]:
|
||||
def serialize_frozen_trial(
|
||||
study_id: int, trial: FrozenTrial, study_system_attrs: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
serialized = {
|
||||
"trial_id": trial._trial_id,
|
||||
"study_id": study_id,
|
||||
@@ -108,6 +113,7 @@ def serialize_frozen_trial(study_id: int, trial: FrozenTrial) -> dict[str, Any]:
|
||||
"params": [{"name": name, "value": str(value)} for name, value in trial.params.items()],
|
||||
"user_attrs": serialize_attrs(trial.user_attrs),
|
||||
"system_attrs": serialize_attrs(getattr(trial, "_system_attrs", {})),
|
||||
"note": note.get_note_from_system_attrs(study_system_attrs, trial._trial_id),
|
||||
}
|
||||
|
||||
serialized_intermediate_values: list[IntermediateValue] = []
|
||||
|
||||
@@ -3,14 +3,17 @@ import { useSnackbar } from "notistack"
|
||||
import {
|
||||
getStudyDetailAPI,
|
||||
getStudySummariesAPI,
|
||||
getParamImportances,
|
||||
createNewStudyAPI,
|
||||
deleteStudyAPI,
|
||||
saveNoteAPI,
|
||||
saveStudyNoteAPI,
|
||||
saveTrialNoteAPI,
|
||||
} from "./apiClient"
|
||||
import {
|
||||
graphVisibilityState,
|
||||
studyDetailsState,
|
||||
studySummariesState,
|
||||
paramImportanceState,
|
||||
} from "./state"
|
||||
|
||||
const localStorageGraphVisibility = "graphVisibility"
|
||||
@@ -23,6 +26,8 @@ export const actionCreator = () => {
|
||||
useRecoilState<StudyDetails>(studyDetailsState)
|
||||
const [graphVisibility, setGraphVisibility] =
|
||||
useRecoilState<GraphVisibility>(graphVisibilityState)
|
||||
const [paramImportance, setParamImportance] =
|
||||
useRecoilState<StudyParamImportance>(paramImportanceState)
|
||||
|
||||
const setStudyDetailState = (studyId: number, study: StudyDetail) => {
|
||||
const newVal = Object.assign({}, studyDetails)
|
||||
@@ -30,6 +35,28 @@ export const actionCreator = () => {
|
||||
setStudyDetails(newVal)
|
||||
}
|
||||
|
||||
const setTrialNote = (studyId: number, index: number, note: Note) => {
|
||||
const newTrial: Trial = Object.assign(
|
||||
{},
|
||||
studyDetails[studyId].trials[index]
|
||||
)
|
||||
newTrial.note = note
|
||||
const newTrials: Trial[] = [...studyDetails[studyId].trials]
|
||||
newTrials[index] = newTrial
|
||||
const newStudy: StudyDetail = Object.assign({}, studyDetails[studyId])
|
||||
newStudy.trials = newTrials
|
||||
setStudyDetailState(studyId, newStudy)
|
||||
}
|
||||
|
||||
const setStudyParamImportanceState = (
|
||||
studyId: number,
|
||||
importance: ParamImportance[][]
|
||||
) => {
|
||||
const newVal = Object.assign({}, paramImportance)
|
||||
newVal[studyId] = importance
|
||||
setParamImportance(newVal)
|
||||
}
|
||||
|
||||
const updateStudySummaries = (successMsg?: string) => {
|
||||
getStudySummariesAPI()
|
||||
.then((studySummaries: StudySummary[]) => {
|
||||
@@ -77,6 +104,22 @@ export const actionCreator = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const updateParamImportance = (studyId: number) => {
|
||||
getParamImportances(studyId)
|
||||
.then((importance) => {
|
||||
setStudyParamImportanceState(studyId, importance)
|
||||
})
|
||||
.catch((err) => {
|
||||
const reason = err.response?.data.reason
|
||||
enqueueSnackbar(
|
||||
`Failed to load hyperparameter importance (reason=${reason})`,
|
||||
{
|
||||
variant: "error",
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const createNewStudy = (studyName: string, directions: StudyDirection[]) => {
|
||||
createNewStudyAPI(studyName, directions)
|
||||
.then((study_summary) => {
|
||||
@@ -128,8 +171,8 @@ export const actionCreator = () => {
|
||||
localStorage.setItem(localStorageGraphVisibility, JSON.stringify(value))
|
||||
}
|
||||
|
||||
const saveNote = (studyId: number, note: Note): Promise<void> => {
|
||||
return saveNoteAPI(studyId, note)
|
||||
const saveStudyNote = (studyId: number, note: Note): Promise<void> => {
|
||||
return saveStudyNoteAPI(studyId, note)
|
||||
.then(() => {
|
||||
const newStudy = Object.assign({}, studyDetails[studyId])
|
||||
newStudy.note = note
|
||||
@@ -154,14 +197,64 @@ export const actionCreator = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const saveTrialNote = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
note: Note
|
||||
): Promise<void> => {
|
||||
return saveTrialNoteAPI(studyId, trialId, note)
|
||||
.then(() => {
|
||||
const index = studyDetails[studyId].trials.findIndex(
|
||||
(t) => t.trial_id === trialId
|
||||
)
|
||||
if (index === -1) {
|
||||
enqueueSnackbar(`Unexpected error happens. Please reload the page.`, {
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
setTrialNote(studyId, index, note)
|
||||
enqueueSnackbar(`Success to save the note`, {
|
||||
variant: "success",
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.dir(err)
|
||||
if (err.response.status === 409) {
|
||||
const index = studyDetails[studyId].trials.findIndex(
|
||||
(t) => t.trial_id === trialId
|
||||
)
|
||||
if (index === -1) {
|
||||
enqueueSnackbar(
|
||||
`Unexpected error happens. Please reload the page.`,
|
||||
{
|
||||
variant: "error",
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
setTrialNote(studyId, index, note)
|
||||
}
|
||||
const reason = err.response?.data.reason
|
||||
if (reason !== undefined) {
|
||||
enqueueSnackbar(`Failed: ${reason}`, {
|
||||
variant: "error",
|
||||
})
|
||||
}
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
updateStudyDetail,
|
||||
updateStudySummaries,
|
||||
updateParamImportance,
|
||||
createNewStudy,
|
||||
deleteStudy,
|
||||
getGraphVisibility,
|
||||
saveGraphVisibility,
|
||||
saveNote,
|
||||
saveStudyNote,
|
||||
saveTrialNote,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ interface TrialResponse {
|
||||
params: TrialParam[]
|
||||
user_attrs: Attribute[]
|
||||
system_attrs: Attribute[]
|
||||
note: Note
|
||||
}
|
||||
|
||||
const convertTrialResponse = (res: TrialResponse): Trial => {
|
||||
@@ -33,6 +34,7 @@ const convertTrialResponse = (res: TrialResponse): Trial => {
|
||||
params: res.params,
|
||||
user_attrs: res.user_attrs,
|
||||
system_attrs: res.system_attrs,
|
||||
note: res.note,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +48,7 @@ interface StudyDetailResponse {
|
||||
union_search_space: SearchSpace[]
|
||||
union_user_attrs: AttributeSpec[]
|
||||
has_intermediate_values: boolean
|
||||
note: {
|
||||
version: number
|
||||
body: string
|
||||
}
|
||||
note: Note
|
||||
}
|
||||
|
||||
export const getStudyDetailAPI = (
|
||||
@@ -183,7 +182,7 @@ export const deleteStudyAPI = (studyId: number) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const saveNoteAPI = (
|
||||
export const saveStudyNoteAPI = (
|
||||
studyId: number,
|
||||
note: { version: number; body: string }
|
||||
): Promise<void> => {
|
||||
@@ -194,25 +193,28 @@ export const saveNoteAPI = (
|
||||
})
|
||||
}
|
||||
|
||||
export const saveTrialNoteAPI = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
note: { version: number; body: string }
|
||||
): Promise<void> => {
|
||||
return axiosInstance
|
||||
.put<void>(`/api/studies/${studyId}/${trialId}/note`, note)
|
||||
.then((res) => {
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
interface ParamImportancesResponse {
|
||||
target_name: string
|
||||
param_importances: ParamImportance[]
|
||||
param_importances: ParamImportance[][]
|
||||
}
|
||||
|
||||
export const getParamImportances = (
|
||||
studyId: number,
|
||||
objectiveId = 0
|
||||
): Promise<ParamImportances> => {
|
||||
studyId: number
|
||||
): Promise<ParamImportance[][]> => {
|
||||
return axiosInstance
|
||||
.get<ParamImportancesResponse>(
|
||||
`/api/studies/${studyId}/param_importances`,
|
||||
{
|
||||
params: {
|
||||
objective_id: objectiveId,
|
||||
},
|
||||
}
|
||||
)
|
||||
.get<ParamImportancesResponse>(`/api/studies/${studyId}/param_importances`)
|
||||
.then((res) => {
|
||||
return res.data
|
||||
return res.data.param_importances
|
||||
})
|
||||
}
|
||||
|
||||
@@ -76,7 +76,16 @@ export const App: FC = () => {
|
||||
children={
|
||||
<StudyDetailBeta
|
||||
toggleColorMode={toggleColorMode}
|
||||
page={"trials"}
|
||||
page={"trialList"}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={URL_PREFIX + "/studies/:studyId/trialTable"}
|
||||
children={
|
||||
<StudyDetailBeta
|
||||
toggleColorMode={toggleColorMode}
|
||||
page={"trialTable"}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -14,9 +14,10 @@ import ListItem from "@mui/material/ListItem"
|
||||
import ListItemButton from "@mui/material/ListItemButton"
|
||||
import ListItemIcon from "@mui/material/ListItemIcon"
|
||||
import ListItemText from "@mui/material/ListItemText"
|
||||
import { reloadIntervalState } from "../state"
|
||||
import { drawerOpenState, reloadIntervalState } from "../state"
|
||||
import { Link } from "react-router-dom"
|
||||
import AutoGraphIcon from "@mui/icons-material/AutoGraph"
|
||||
import ViewListIcon from "@mui/icons-material/ViewList"
|
||||
import SyncIcon from "@mui/icons-material/Sync"
|
||||
import SyncDisabledIcon from "@mui/icons-material/SyncDisabled"
|
||||
import Brightness4Icon from "@mui/icons-material/Brightness4"
|
||||
@@ -104,12 +105,12 @@ const Drawer = styled(MuiDrawer, {
|
||||
export const AppDrawer: FC<{
|
||||
studyId?: number
|
||||
toggleColorMode: () => void
|
||||
page?: "history" | "analytics" | "trials" | "note"
|
||||
page?: PageId
|
||||
toolbar: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
}> = ({ studyId, toggleColorMode, page, toolbar, children }) => {
|
||||
const theme = useTheme()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [open, setOpen] = useRecoilState<boolean>(drawerOpenState)
|
||||
const [reloadInterval, updateReloadInterval] =
|
||||
useRecoilState<number>(reloadIntervalState)
|
||||
|
||||
@@ -199,17 +200,30 @@ export const AppDrawer: FC<{
|
||||
<ListItemText primary="Analytics" sx={styleListItemText} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem key="Table" disablePadding sx={styleListItem}>
|
||||
<ListItem key="TableList" disablePadding sx={styleListItem}>
|
||||
<ListItemButton
|
||||
component={Link}
|
||||
to={`${URL_PREFIX}/studies/${studyId}/trials`}
|
||||
sx={styleListItemButton}
|
||||
selected={page === "trials"}
|
||||
selected={page === "trialList"}
|
||||
>
|
||||
<ListItemIcon sx={styleListItemIcon}>
|
||||
<ViewListIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Trials (List)" sx={styleListItemText} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem key="TrialTable" disablePadding sx={styleListItem}>
|
||||
<ListItemButton
|
||||
component={Link}
|
||||
to={`${URL_PREFIX}/studies/${studyId}/trialTable`}
|
||||
sx={styleListItemButton}
|
||||
selected={page === "trialTable"}
|
||||
>
|
||||
<ListItemIcon sx={styleListItemIcon}>
|
||||
<TableViewIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Trials" sx={styleListItemText} />
|
||||
<ListItemText primary="Trials (Table)" sx={styleListItemText} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem key="Note" disablePadding sx={styleListItem}>
|
||||
@@ -301,7 +315,10 @@ export const AppDrawer: FC<{
|
||||
<ListItemIcon sx={styleListItemIcon}>
|
||||
<ClearIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Quit Beta UI" sx={styleListItemText} />
|
||||
<ListItemText
|
||||
primary="Switch to stable UI"
|
||||
sx={styleListItemText}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
@@ -127,6 +127,7 @@ export const GraphHistory: FC<{
|
||||
control={
|
||||
<Checkbox
|
||||
checked={!filterPrunedTrial}
|
||||
disabled={!study?.has_intermediate_values}
|
||||
onChange={handleFilterPrunedChange}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -10,51 +10,141 @@ import {
|
||||
SelectChangeEvent,
|
||||
useTheme,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
} from "@mui/material"
|
||||
import Grid2 from "@mui/material/Unstable_Grid2"
|
||||
|
||||
import { getParamImportances } from "../apiClient"
|
||||
import { plotlyDarkTemplate } from "./PlotlyDarkMode"
|
||||
import { useSnackbar } from "notistack"
|
||||
import { actionCreator } from "../action"
|
||||
import { useParamImportanceValue, useStudyDirections } from "../state"
|
||||
const plotDomId = "graph-hyperparameter-importances"
|
||||
const getPlotDomId = (objectiveId: number) => `graph-importance-${objectiveId}`
|
||||
|
||||
export const GraphHyperparameterImportanceBeta: FC<{
|
||||
studyId: number
|
||||
study: StudyDetail | null
|
||||
graphHeight: string
|
||||
}> = ({ studyId, study = null, graphHeight }) => {
|
||||
const theme = useTheme()
|
||||
const action = actionCreator()
|
||||
const importances = useParamImportanceValue(studyId)
|
||||
const numCompletedTrials =
|
||||
study?.trials.filter((t) => t.state === "Complete").length || 0
|
||||
const nObjectives = useStudyDirections(studyId)?.length
|
||||
|
||||
useEffect(() => {
|
||||
action.updateParamImportance(studyId)
|
||||
}, [numCompletedTrials])
|
||||
|
||||
useEffect(() => {
|
||||
if (importances !== null && nObjectives === importances.length) {
|
||||
plotParamImportancesBeta(importances, theme.palette.mode)
|
||||
}
|
||||
}, [nObjectives, importances, theme.palette.mode])
|
||||
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: nObjectives || 1 }, (_, i) => {
|
||||
let title = `Importance for the Objective Value`
|
||||
if (nObjectives != null && nObjectives > 1) {
|
||||
title = `Importance for the Objective ${i}`
|
||||
}
|
||||
return (
|
||||
<Grid2 key={i} xs={6}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: "1em 0", fontWeight: 600, textAlign: "center" }}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Box id={getPlotDomId(i)} sx={{ height: graphHeight }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const plotParamImportancesBeta = (
|
||||
importances: ParamImportance[][],
|
||||
mode: string
|
||||
) => {
|
||||
const layout: Partial<plotly.Layout> = {
|
||||
xaxis: {
|
||||
title: "Hyperparameter Importance",
|
||||
},
|
||||
yaxis: {
|
||||
title: "Hyperparameter",
|
||||
automargin: true,
|
||||
},
|
||||
margin: {
|
||||
l: 50,
|
||||
t: 0,
|
||||
r: 50,
|
||||
b: 50,
|
||||
},
|
||||
showlegend: false,
|
||||
template: mode === "dark" ? plotlyDarkTemplate : {},
|
||||
}
|
||||
|
||||
importances.forEach((importance, objectiveId) => {
|
||||
if (document.getElementById(getPlotDomId(objectiveId)) === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const reversed = [...importance].reverse()
|
||||
const importance_values = reversed.map((p) => p.importance)
|
||||
const param_names = reversed.map((p) => p.name)
|
||||
const param_hover_templates = reversed.map(
|
||||
(p) => `${p.name} (${p.distribution}): ${p.importance} <extra></extra>`
|
||||
)
|
||||
const plotData: Partial<plotly.PlotData>[] = [
|
||||
{
|
||||
type: "bar",
|
||||
orientation: "h",
|
||||
x: importance_values,
|
||||
y: param_names,
|
||||
text: importance_values.map((v) => String(v.toFixed(2))),
|
||||
textposition: "outside",
|
||||
hovertemplate: param_hover_templates,
|
||||
marker: {
|
||||
color: "rgb(66,146,198)",
|
||||
},
|
||||
},
|
||||
]
|
||||
plotly.react(getPlotDomId(objectiveId), plotData, layout)
|
||||
})
|
||||
}
|
||||
|
||||
export const GraphHyperparameterImportances: FC<{
|
||||
study: StudyDetail | null
|
||||
studyId: number
|
||||
}> = ({ study = null, studyId }) => {
|
||||
const theme = useTheme()
|
||||
const action = actionCreator()
|
||||
const importances = useParamImportanceValue(studyId)
|
||||
const [objectiveId, setObjectiveId] = useState<number>(0)
|
||||
const numCompletedTrials =
|
||||
study?.trials.filter((t) => t.state === "Complete").length || 0
|
||||
const [importances, setImportances] = useState<ParamImportances | null>(null)
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
|
||||
const handleObjectiveChange = (event: SelectChangeEvent<number>) => {
|
||||
setObjectiveId(event.target.value as number)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (numCompletedTrials > 0) {
|
||||
getParamImportances(studyId, objectiveId)
|
||||
.then((p) => {
|
||||
setImportances(p)
|
||||
})
|
||||
.catch((err) => {
|
||||
const reason = err.response?.data.reason
|
||||
enqueueSnackbar(
|
||||
`Failed to load hyperparameter importance (reason=${reason})`,
|
||||
{
|
||||
variant: "error",
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}, [numCompletedTrials, objectiveId, theme.palette.mode])
|
||||
action.updateParamImportance(studyId)
|
||||
}, [numCompletedTrials])
|
||||
|
||||
useEffect(() => {
|
||||
if (importances !== null) {
|
||||
plotParamImportances(importances, theme.palette.mode)
|
||||
if (importances !== null && importances.length > objectiveId) {
|
||||
plotParamImportances(importances[objectiveId], theme.palette.mode)
|
||||
}
|
||||
}, [importances, theme.palette.mode])
|
||||
}, [importances, objectiveId, theme.palette.mode])
|
||||
|
||||
return (
|
||||
<Grid container direction="row">
|
||||
@@ -88,25 +178,20 @@ export const GraphHyperparameterImportances: FC<{
|
||||
)
|
||||
}
|
||||
|
||||
const plotParamImportances = (
|
||||
paramsImportanceData: ParamImportances,
|
||||
mode: string
|
||||
) => {
|
||||
const plotParamImportances = (importance: ParamImportance[], mode: string) => {
|
||||
if (document.getElementById(plotDomId) === null) {
|
||||
return
|
||||
}
|
||||
const param_importances = [
|
||||
...paramsImportanceData.param_importances,
|
||||
].reverse()
|
||||
const importance_values = param_importances.map((p) => p.importance)
|
||||
const param_names = param_importances.map((p) => p.name)
|
||||
const param_hover_templates = param_importances.map(
|
||||
const reversed = [...importance].reverse()
|
||||
const importance_values = reversed.map((p) => p.importance)
|
||||
const param_names = reversed.map((p) => p.name)
|
||||
const param_hover_templates = reversed.map(
|
||||
(p) => `${p.name} (${p.distribution}): ${p.importance} <extra></extra>`
|
||||
)
|
||||
|
||||
const layout: Partial<plotly.Layout> = {
|
||||
xaxis: {
|
||||
title: `Importance for ${paramsImportanceData.target_name}`,
|
||||
title: `Importance for the Objective Value`,
|
||||
},
|
||||
yaxis: {
|
||||
title: "Hyperparameter",
|
||||
|
||||
@@ -61,7 +61,6 @@ export const GraphSlice: FC<{
|
||||
}
|
||||
|
||||
const handleLogYScaleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
setLogYScale(!logYScale)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,22 +3,96 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
IconButton,
|
||||
SxProps,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import React, { FC, createRef, useState, useEffect } from "react"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import remarkGfm from "remark-gfm"
|
||||
import LoadingButton from "@mui/lab/LoadingButton"
|
||||
import SaveIcon from "@mui/icons-material/Save"
|
||||
import EditIcon from "@mui/icons-material/Edit"
|
||||
import CloseIcon from "@mui/icons-material/Close"
|
||||
import Divider from "@mui/material/Divider"
|
||||
import { Theme } from "@mui/material/styles"
|
||||
import {
|
||||
CodeComponent,
|
||||
ReactMarkdownNames,
|
||||
} from "react-markdown/lib/ast-to-react"
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import { darcula } from "react-syntax-highlighter/dist/esm/styles/prism"
|
||||
|
||||
import { actionCreator } from "../action"
|
||||
|
||||
export const Note: FC<{
|
||||
const CodeBlock: CodeComponent | ReactMarkdownNames = ({
|
||||
inline,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const match = /language-(\w+)/.exec(className || "")
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={darcula}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
|
||||
export const TrialNote: FC<{
|
||||
studyId: number
|
||||
trialId: number
|
||||
latestNote: Note
|
||||
cardSx?: SxProps<Theme>
|
||||
}> = ({ studyId, trialId, latestNote, cardSx }) => {
|
||||
return (
|
||||
<NoteBase
|
||||
studyId={studyId}
|
||||
trialId={trialId}
|
||||
latestNote={latestNote}
|
||||
minRows={30}
|
||||
cardSx={cardSx}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const StudyNote: FC<{
|
||||
studyId: number
|
||||
latestNote: Note
|
||||
minRows: number
|
||||
}> = ({ studyId, latestNote, minRows }) => {
|
||||
cardSx?: SxProps<Theme>
|
||||
}> = ({ studyId, latestNote, minRows, cardSx }) => {
|
||||
return (
|
||||
<NoteBase
|
||||
studyId={studyId}
|
||||
latestNote={latestNote}
|
||||
minRows={minRows}
|
||||
cardSx={cardSx}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const NoteBase: FC<{
|
||||
studyId: number
|
||||
trialId?: number
|
||||
latestNote: Note
|
||||
minRows: number
|
||||
cardSx?: SxProps<Theme>
|
||||
}> = ({ studyId, trialId, latestNote, minRows, cardSx }) => {
|
||||
const theme = useTheme()
|
||||
const [renderMarkdown, setRenderMarkdown] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [edited, setEdited] = useState(false)
|
||||
const [curNote, setCurNote] = useState({ version: 0, body: "" })
|
||||
@@ -45,10 +119,17 @@ export const Note: FC<{
|
||||
body: textAreaRef.current ? textAreaRef.current.value : "",
|
||||
}
|
||||
setSaving(true)
|
||||
action
|
||||
.saveNote(studyId, newNote)
|
||||
|
||||
let actionResponse: Promise<void>
|
||||
if (trialId === undefined) {
|
||||
actionResponse = action.saveStudyNote(studyId, newNote)
|
||||
} else {
|
||||
actionResponse = action.saveTrialNote(studyId, trialId, newNote)
|
||||
}
|
||||
actionResponse
|
||||
.then(() => {
|
||||
setCurNote(newNote)
|
||||
setRenderMarkdown(true)
|
||||
window.onbeforeunload = null
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -65,17 +146,27 @@ export const Note: FC<{
|
||||
window.onbeforeunload = null
|
||||
}
|
||||
|
||||
return (
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" sx={{ fontSize: "1.25rem", fontWeight: 600 }}>
|
||||
Note
|
||||
</Typography>
|
||||
let content
|
||||
if (renderMarkdown) {
|
||||
const defaultBody =
|
||||
"*A markdown editor for taking a memo, related to the study. Click the 'Edit' button in the upper right corner to access the editor.*"
|
||||
content = (
|
||||
<ReactMarkdown
|
||||
children={latestNote.body || defaultBody}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{ code: CodeBlock }}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
content = (
|
||||
<>
|
||||
<TextField
|
||||
disabled={saving}
|
||||
minRows={minRows}
|
||||
multiline={true}
|
||||
placeholder="Description about the study... (This note is saved to study's system_attrs)"
|
||||
placeholder={`Description about the ${
|
||||
trialId === undefined ? "study" : "trial"
|
||||
}...`}
|
||||
sx={{ width: "100%", margin: `${theme.spacing(1)} 0` }}
|
||||
inputProps={{ style: { resize: "vertical" } }}
|
||||
inputRef={textAreaRef}
|
||||
@@ -123,6 +214,34 @@ export const Note: FC<{
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card sx={{ margin: theme.spacing(2), ...cardSx }}>
|
||||
<CardHeader
|
||||
title="Note"
|
||||
action={
|
||||
!renderMarkdown ? (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setRenderMarkdown(true)
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={() => setRenderMarkdown(false)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
sx={{ paddingBottom: 0 }}
|
||||
/>
|
||||
<CardContent sx={{ paddingTop: theme.spacing(1) }}>
|
||||
<Divider />
|
||||
{content}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ import { GraphIntermediateValues } from "./GraphIntermediateValues"
|
||||
import { GraphSlice } from "./GraphSlice"
|
||||
import { GraphHistory } from "./GraphHistory"
|
||||
import { GraphParetoFront } from "./GraphParetoFront"
|
||||
import { Note } from "./Note"
|
||||
import { StudyNote } from "./Note"
|
||||
import { actionCreator } from "../action"
|
||||
import {
|
||||
graphVisibilityState,
|
||||
@@ -235,7 +235,7 @@ export const StudyDetail: FC<{
|
||||
<TrialTable studyDetail={studyDetail} />
|
||||
</Card>
|
||||
{studyDetail !== null ? (
|
||||
<Note
|
||||
<StudyNote
|
||||
studyId={studyIdNumber}
|
||||
latestNote={studyDetail.note}
|
||||
minRows={5}
|
||||
|
||||
@@ -2,12 +2,11 @@ import React, { FC, useEffect } from "react"
|
||||
import { useRecoilValue } from "recoil"
|
||||
import { Link, useParams } from "react-router-dom"
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Box,
|
||||
Typography,
|
||||
useTheme,
|
||||
ListItem,
|
||||
IconButton,
|
||||
} from "@mui/material"
|
||||
import Grid2 from "@mui/material/Unstable_Grid2"
|
||||
@@ -15,40 +14,31 @@ import ChevronRightIcon from "@mui/icons-material/ChevronRight"
|
||||
import HomeIcon from "@mui/icons-material/Home"
|
||||
|
||||
import { GraphHistory } from "./GraphHistory"
|
||||
import { Note } from "./Note"
|
||||
import { StudyNote } from "./Note"
|
||||
import { actionCreator } from "../action"
|
||||
import {
|
||||
reloadIntervalState,
|
||||
studyDetailsState,
|
||||
studySummariesState,
|
||||
useStudyDetailValue,
|
||||
useStudyDirections,
|
||||
useStudyName,
|
||||
useStudySummaryValue,
|
||||
} from "../state"
|
||||
import { TrialTable } from "./TrialTable"
|
||||
import { AppDrawer } from "./AppDrawer"
|
||||
import { GraphParallelCoordinate } from "./GraphParallelCoordinate"
|
||||
import { Contour } from "./GraphContour"
|
||||
import { GraphHyperparameterImportances } from "./GraphHyperparameterImportances"
|
||||
import { GraphHyperparameterImportanceBeta } from "./GraphHyperparameterImportances"
|
||||
import { GraphSlice } from "./GraphSlice"
|
||||
import { GraphParetoFront } from "./GraphParetoFront"
|
||||
import { DataGrid, DataGridColumn } from "./DataGrid"
|
||||
import List from "@mui/material/List"
|
||||
import { GraphIntermediateValues } from "./GraphIntermediateValues"
|
||||
import { Edf } from "./GraphEdf"
|
||||
import { TrialList } from "./TrialList"
|
||||
|
||||
interface ParamTypes {
|
||||
studyId: string
|
||||
}
|
||||
|
||||
type PageId = "history" | "analytics" | "trials" | "note"
|
||||
|
||||
const useStudyDetailValue = (studyId: number): StudyDetail | null => {
|
||||
const studyDetails = useRecoilValue<StudyDetails>(studyDetailsState)
|
||||
return studyDetails[studyId] || null
|
||||
}
|
||||
|
||||
const useStudySummaryValue = (studyId: number): StudySummary | null => {
|
||||
const studySummaries = useRecoilValue<StudySummary[]>(studySummariesState)
|
||||
return studySummaries.find((s) => s.study_id == studyId) || null
|
||||
}
|
||||
|
||||
export const StudyDetailBeta: FC<{
|
||||
toggleColorMode: () => void
|
||||
page: PageId
|
||||
@@ -60,20 +50,19 @@ export const StudyDetailBeta: FC<{
|
||||
const studyDetail = useStudyDetailValue(studyIdNumber)
|
||||
const reloadInterval = useRecoilValue<number>(reloadIntervalState)
|
||||
const studySummary = useStudySummaryValue(studyIdNumber)
|
||||
const directions = studyDetail?.directions || studySummary?.directions || null
|
||||
const directions = useStudyDirections(studyIdNumber)
|
||||
const studyName = useStudyName(studyIdNumber)
|
||||
const userAttrs = studySummary?.user_attrs || []
|
||||
|
||||
const title =
|
||||
studyDetail !== null || studySummary !== null
|
||||
? `${studyDetail?.name || studySummary?.study_name} (id=${studyId})`
|
||||
: `Study #${studyId}`
|
||||
studyName !== null ? `${studyName} (id=${studyId})` : `Study #${studyId}`
|
||||
|
||||
useEffect(() => {
|
||||
action.updateStudyDetail(studyIdNumber)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (reloadInterval < 0 || page === "trials") {
|
||||
if (reloadInterval < 0 || page === "trialTable" || page === "trialList") {
|
||||
return
|
||||
}
|
||||
const intervalId = setInterval(function () {
|
||||
@@ -92,6 +81,13 @@ export const StudyDetailBeta: FC<{
|
||||
if (page === "history") {
|
||||
content = (
|
||||
<Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}>
|
||||
{directions !== null && directions.length > 1 ? (
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<GraphParetoFront study={studyDetail} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
<Card
|
||||
sx={{
|
||||
margin: theme.spacing(2),
|
||||
@@ -110,17 +106,21 @@ export const StudyDetailBeta: FC<{
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
{directions !== null && directions.length > 1 ? (
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<GraphParetoFront study={studyDetail} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 xs={6}>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<Grid2 container spacing={2} sx={{ padding: theme.spacing(0, 2) }}>
|
||||
<GraphHyperparameterImportanceBeta
|
||||
studyId={studyIdNumber}
|
||||
study={studyDetail}
|
||||
graphHeight="450px"
|
||||
/>
|
||||
<Grid2 xs={6} spacing={2}>
|
||||
<Card>
|
||||
<CardContent
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{studyDetail !== null &&
|
||||
studyDetail.best_trials.length === 1 && (
|
||||
<>
|
||||
@@ -131,37 +131,89 @@ export const StudyDetailBeta: FC<{
|
||||
Best Trial
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{ fontWeight: 600 }}
|
||||
variant="h3"
|
||||
sx={{ fontWeight: 600, marginBottom: theme.spacing(2) }}
|
||||
color="secondary"
|
||||
>
|
||||
{studyDetail.best_trials[0].values}
|
||||
</Typography>
|
||||
<List>
|
||||
{studyDetail.best_trials[0].params.map((param) => (
|
||||
<ListItem>
|
||||
{param.name} {param.value}
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Typography>
|
||||
number={studyDetail.best_trials[0].number}
|
||||
</Typography>
|
||||
<Typography>
|
||||
trial_id={studyDetail.best_trials[0].trial_id}
|
||||
</Typography>
|
||||
<Typography>
|
||||
Params = [
|
||||
{studyDetail.best_trials[0].params
|
||||
.map((p) => `${p.name}: ${p.value}`)
|
||||
.join(", ")}
|
||||
]
|
||||
</Typography>
|
||||
<Typography>
|
||||
Intermediate Values = [
|
||||
{studyDetail.best_trials[0].intermediate_values
|
||||
.map((p) => `${p.step}: ${p.value}`)
|
||||
.join(", ")}
|
||||
]
|
||||
</Typography>
|
||||
<Typography>
|
||||
User Attributes = [
|
||||
{studyDetail.best_trials[0].user_attrs
|
||||
.map((p) => `${p.key}: ${p.value}`)
|
||||
.join(", ")}
|
||||
]
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{studyDetail !== null && studyDetail.best_trials.length > 1 && (
|
||||
{studyDetail !== null && studyDetail.directions.length > 1 && (
|
||||
<>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: "1em 0", fontWeight: 600 }}
|
||||
>
|
||||
Best Trials
|
||||
Best Trials ({studyDetail.best_trials.length} trials)
|
||||
</Typography>
|
||||
{studyDetail.best_trials.map((trial, i) => (
|
||||
<Card
|
||||
key={i}
|
||||
sx={{
|
||||
border: "1px solid rgba(128,128,128,0.5)",
|
||||
margin: theme.spacing(1, 0),
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography variant="h6">
|
||||
Trial number={trial.number} (trial_id=
|
||||
{trial.trial_id})
|
||||
</Typography>
|
||||
<Typography>
|
||||
Objective Values = [{trial.values?.join(", ")}]
|
||||
</Typography>
|
||||
<Typography>
|
||||
Params = [
|
||||
{trial.params
|
||||
.map((p) => `${p.name}: ${p.value}`)
|
||||
.join(", ")}
|
||||
]
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
<Grid2 xs={6}>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<Card>
|
||||
<CardContent
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: "1em 0", fontWeight: 600 }}
|
||||
@@ -185,17 +237,6 @@ export const StudyDetailBeta: FC<{
|
||||
} else if (page === "analytics") {
|
||||
content = (
|
||||
<Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}>
|
||||
<Typography variant="h5" sx={{ margin: theme.spacing(2) }}>
|
||||
Hyperparameter Importance
|
||||
</Typography>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<GraphHyperparameterImportances
|
||||
study={studyDetail}
|
||||
studyId={studyIdNumber}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Typography variant="h5" sx={{ margin: theme.spacing(2) }}>
|
||||
Hyperparameter Relationships
|
||||
</Typography>
|
||||
@@ -214,25 +255,35 @@ export const StudyDetailBeta: FC<{
|
||||
<Contour study={studyDetail} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Typography variant="h5" sx={{ margin: theme.spacing(2) }}>
|
||||
Empirical Distribution of the Objective Value
|
||||
</Typography>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<Edf study={studyDetail} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
)
|
||||
} else if (page === "trials") {
|
||||
} else if (page === "trialTable") {
|
||||
content = (
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardContent>
|
||||
<TrialTable studyDetail={studyDetail} />
|
||||
<TrialTable studyDetail={studyDetail} initialRowsPerPage={50} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
} else if (page === "note") {
|
||||
content =
|
||||
studyDetail !== null ? (
|
||||
<Note
|
||||
studyId={studyIdNumber}
|
||||
latestNote={studyDetail.note}
|
||||
minRows={30}
|
||||
/>
|
||||
) : null
|
||||
} else if (page === "trialList") {
|
||||
content = <TrialList studyDetail={studyDetail} />
|
||||
} else if (page === "note" && studyDetail !== null) {
|
||||
content = (
|
||||
<StudyNote
|
||||
studyId={studyIdNumber}
|
||||
latestNote={studyDetail.note}
|
||||
minRows={30}
|
||||
cardSx={{ height: "90vh" }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const toolbar = (
|
||||
|
||||
@@ -23,7 +23,6 @@ import { DebouncedInputTextField } from "./Debounce"
|
||||
import { studySummariesState } from "../state"
|
||||
import Brightness7Icon from "@mui/icons-material/Brightness7"
|
||||
import Brightness4Icon from "@mui/icons-material/Brightness4"
|
||||
import { useSnackbar } from "notistack"
|
||||
import { useDeleteStudyDialog } from "./DeleteStudyDialog"
|
||||
import { useCreateStudyDialog } from "./CreateStudyDialog"
|
||||
|
||||
@@ -31,7 +30,6 @@ export const StudyList: FC<{
|
||||
toggleColorMode: () => void
|
||||
}> = ({ toggleColorMode }) => {
|
||||
const theme = useTheme()
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
|
||||
const [studyFilterText, setStudyFilterText] = React.useState<string>("")
|
||||
const studyFilter = (row: StudySummary) => {
|
||||
@@ -150,15 +148,6 @@ export const StudyList: FC<{
|
||||
)
|
||||
}
|
||||
|
||||
const sayThankYouForBetaUsers = () => {
|
||||
enqueueSnackbar(
|
||||
`Thanks for testing our beta UI. Share your feedback via a GitHub issue.`,
|
||||
{
|
||||
variant: "success",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBar position="static">
|
||||
@@ -225,11 +214,7 @@ export const StudyList: FC<{
|
||||
<CardContent>
|
||||
<Typography>
|
||||
{`We would appreciate your feedback on our beta UI. Click `}
|
||||
<Link
|
||||
to={`${URL_PREFIX}/beta`}
|
||||
style={{ color: linkColor }}
|
||||
onClick={sayThankYouForBetaUsers}
|
||||
>
|
||||
<Link to={`${URL_PREFIX}/beta`} style={{ color: linkColor }}>
|
||||
here
|
||||
</Link>
|
||||
{" to try it out and share your thoughts."}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "@mui/material"
|
||||
import { Delete, Refresh, Search } from "@mui/icons-material"
|
||||
import SortIcon from "@mui/icons-material/Sort"
|
||||
import HomeIcon from "@mui/icons-material/Home"
|
||||
import AddBoxIcon from "@mui/icons-material/AddBox"
|
||||
|
||||
import { actionCreator } from "../action"
|
||||
@@ -101,11 +102,7 @@ export const StudyListBeta: FC<{
|
||||
</Box>
|
||||
)
|
||||
|
||||
const toolbar = (
|
||||
<Typography variant="h5" noWrap component="div">
|
||||
Optuna Dashboard (Beta ver.)
|
||||
</Typography>
|
||||
)
|
||||
const toolbar = <HomeIcon sx={{ margin: theme.spacing(0, 1) }} />
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex" }}>
|
||||
@@ -153,7 +150,7 @@ export const StudyListBeta: FC<{
|
||||
}}
|
||||
sx={{ marginRight: theme.spacing(2), minWidth: "120px" }}
|
||||
>
|
||||
Refresh
|
||||
Reload
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -181,7 +178,7 @@ export const StudyListBeta: FC<{
|
||||
>
|
||||
<CardContent>
|
||||
<Typography variant="h5">
|
||||
{study.study_id} {study.study_name}
|
||||
{study.study_id}. {study.study_name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import React, { FC, useState } from "react"
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
useTheme,
|
||||
CardHeader,
|
||||
Grid,
|
||||
} from "@mui/material"
|
||||
import Chip from "@mui/material/Chip"
|
||||
import Divider from "@mui/material/Divider"
|
||||
import List from "@mui/material/List"
|
||||
import ListItem from "@mui/material/ListItem"
|
||||
import ListItemButton from "@mui/material/ListItemButton"
|
||||
import ListItemText from "@mui/material/ListItemText"
|
||||
import ListSubheader from "@mui/material/ListSubheader"
|
||||
|
||||
import { TrialNote } from "./Note"
|
||||
import { DataGrid, DataGridColumn } from "./DataGrid"
|
||||
|
||||
export const TrialList: FC<{
|
||||
studyDetail: StudyDetail | null
|
||||
}> = ({ studyDetail }) => {
|
||||
const theme = useTheme()
|
||||
const [selected, setSelected] = useState<number>(0)
|
||||
const trials: Trial[] = studyDetail !== null ? studyDetail.trials : []
|
||||
const trialListWidth = 240
|
||||
|
||||
const isBestTrial = (trialId: number) => {
|
||||
const bestTrialIDs = studyDetail?.best_trials.map((t) => t.trial_id) || []
|
||||
return bestTrialIDs.findIndex((a) => a === trialId) != -1
|
||||
}
|
||||
|
||||
const collapseIntermediateValueColumns: DataGridColumn<TrialIntermediateValue>[] =
|
||||
[
|
||||
{ field: "step", label: "Step", sortable: true },
|
||||
{
|
||||
field: "value",
|
||||
label: "Value",
|
||||
sortable: true,
|
||||
less: (firstEl, secondEl): number => {
|
||||
const firstVal = firstEl.value
|
||||
const secondVal = secondEl.value
|
||||
if (firstVal === secondVal) {
|
||||
return 0
|
||||
}
|
||||
if (firstVal === "nan") {
|
||||
return -1
|
||||
} else if (secondVal === "nan") {
|
||||
return 1
|
||||
}
|
||||
if (firstVal === "-inf" || secondVal === "inf") {
|
||||
return 1
|
||||
} else if (secondVal === "-inf" || firstVal === "inf") {
|
||||
return -1
|
||||
}
|
||||
return firstVal < secondVal ? 1 : -1
|
||||
},
|
||||
},
|
||||
]
|
||||
const collapseAttrColumns: DataGridColumn<Attribute>[] = [
|
||||
{ field: "key", label: "Key", sortable: true },
|
||||
{ field: "value", label: "Value", sortable: true },
|
||||
]
|
||||
|
||||
let content = null
|
||||
if (trials.length > selected) {
|
||||
const trial = trials[selected]
|
||||
content = (
|
||||
<>
|
||||
<Card sx={{ margin: theme.spacing(2) }}>
|
||||
<CardHeader
|
||||
title={`Trial ${trial.number} (trial_id=${trial.trial_id})`}
|
||||
/>
|
||||
<CardContent>
|
||||
{isBestTrial(trial.trial_id) ? (
|
||||
<Chip
|
||||
label={"Best Trial"}
|
||||
color="primary"
|
||||
sx={{ marginBottom: theme.spacing(1) }}
|
||||
size="small"
|
||||
/>
|
||||
) : null}
|
||||
<Typography>
|
||||
Values:{" "}
|
||||
{trial.values?.map((v) => v.toString()).join(" ") || "None"}
|
||||
</Typography>
|
||||
<Typography>
|
||||
Params = [
|
||||
{trial.params.map((p) => `${p.name}: ${p.value}`).join(", ")}]
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TrialNote
|
||||
studyId={trial.study_id}
|
||||
trialId={trial.trial_id}
|
||||
latestNote={trial.note}
|
||||
/>
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{ p: theme.spacing(0, 2) }}
|
||||
>
|
||||
<Grid item xs={6}>
|
||||
<Card>
|
||||
<CardHeader title="Intermediate Values" />
|
||||
<CardContent>
|
||||
<DataGrid<TrialIntermediateValue>
|
||||
columns={collapseIntermediateValueColumns}
|
||||
rows={trial.intermediate_values}
|
||||
keyField={"step"}
|
||||
dense={true}
|
||||
rowsPerPageOption={[5, 10, { label: "All", value: -1 }]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Card>
|
||||
<CardHeader title="User Attributes" />
|
||||
<CardContent>
|
||||
<DataGrid<Attribute>
|
||||
columns={collapseAttrColumns}
|
||||
rows={trial.user_attrs}
|
||||
keyField={"key"}
|
||||
dense={true}
|
||||
rowsPerPageOption={[5, 10, { label: "All", value: -1 }]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: trialListWidth,
|
||||
overflow: "auto",
|
||||
height: `calc(100vh - ${theme.spacing(8)})`,
|
||||
}}
|
||||
>
|
||||
<List>
|
||||
<ListSubheader>{`${
|
||||
studyDetail?.trials.length || 0
|
||||
} Trials`}</ListSubheader>
|
||||
{trials.map((trial, i) => {
|
||||
let color:
|
||||
| "default"
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "error"
|
||||
| "info"
|
||||
| "success"
|
||||
| "warning" = "default"
|
||||
if (trial.state === "Complete") {
|
||||
color = "success"
|
||||
} else if (trial.state === "Running") {
|
||||
color = "secondary"
|
||||
} else if (trial.state === "Waiting") {
|
||||
color = "secondary"
|
||||
} else if (trial.state === "Pruned") {
|
||||
color = "warning"
|
||||
} else if (trial.state === "Fail") {
|
||||
color = "error"
|
||||
}
|
||||
return (
|
||||
<ListItem key={trial.trial_id} disablePadding>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
setSelected(i)
|
||||
}}
|
||||
selected={i === selected}
|
||||
>
|
||||
<ListItemText
|
||||
primary={`Trial ${trial.trial_id}`}
|
||||
secondary={
|
||||
<Box sx={{ padding: theme.spacing(1, 0) }}>
|
||||
<Chip color={color} label={trial.state} size="small" />
|
||||
{isBestTrial(trial.trial_id) ? (
|
||||
<Chip
|
||||
label={"Best Trial"}
|
||||
color="primary"
|
||||
sx={{ marginLeft: theme.spacing(1) }}
|
||||
size="small"
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
height: `calc(100vh - ${theme.spacing(8)})`,
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -3,9 +3,10 @@ import { Typography, Grid, Box } from "@mui/material"
|
||||
|
||||
import { DataGridColumn, DataGrid } from "./DataGrid"
|
||||
|
||||
export const TrialTable: FC<{ studyDetail: StudyDetail | null }> = ({
|
||||
studyDetail,
|
||||
}) => {
|
||||
export const TrialTable: FC<{
|
||||
studyDetail: StudyDetail | null
|
||||
initialRowsPerPage?: number
|
||||
}> = ({ studyDetail, initialRowsPerPage }) => {
|
||||
const trials: Trial[] = studyDetail !== null ? studyDetail.trials : []
|
||||
|
||||
const columns: DataGridColumn<Trial>[] = [
|
||||
@@ -228,6 +229,7 @@ export const TrialTable: FC<{ studyDetail: StudyDetail | null }> = ({
|
||||
]
|
||||
|
||||
const collapseBody = (index: number) => {
|
||||
console.dir(trials)
|
||||
return (
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={6}>
|
||||
@@ -269,6 +271,7 @@ export const TrialTable: FC<{ studyDetail: StudyDetail | null }> = ({
|
||||
keyField={"trial_id"}
|
||||
dense={true}
|
||||
collapseBody={collapseBody}
|
||||
initialRowsPerPage={initialRowsPerPage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { atom } from "recoil"
|
||||
import { atom, useRecoilValue } from "recoil"
|
||||
|
||||
export const studySummariesState = atom<StudySummary[]>({
|
||||
key: "studySummaries",
|
||||
@@ -10,6 +10,11 @@ export const studyDetailsState = atom<StudyDetails>({
|
||||
default: {},
|
||||
})
|
||||
|
||||
export const paramImportanceState = atom<StudyParamImportance>({
|
||||
key: "paramImportance",
|
||||
default: {},
|
||||
})
|
||||
|
||||
export const graphVisibilityState = atom<GraphVisibility>({
|
||||
key: "graphVisibility",
|
||||
default: {
|
||||
@@ -28,3 +33,40 @@ export const reloadIntervalState = atom<number>({
|
||||
key: "reloadInterval",
|
||||
default: 10,
|
||||
})
|
||||
|
||||
export const drawerOpenState = atom<boolean>({
|
||||
key: "drawerOpen",
|
||||
default: false,
|
||||
})
|
||||
|
||||
export const useStudyDetailValue = (studyId: number): StudyDetail | null => {
|
||||
const studyDetails = useRecoilValue<StudyDetails>(studyDetailsState)
|
||||
return studyDetails[studyId] || null
|
||||
}
|
||||
|
||||
export const useStudySummaryValue = (studyId: number): StudySummary | null => {
|
||||
const studySummaries = useRecoilValue<StudySummary[]>(studySummariesState)
|
||||
return studySummaries.find((s) => s.study_id == studyId) || null
|
||||
}
|
||||
|
||||
export const useParamImportanceValue = (
|
||||
studyId: number
|
||||
): ParamImportance[][] | null => {
|
||||
const studyParamImportance =
|
||||
useRecoilValue<StudyParamImportance>(paramImportanceState)
|
||||
return studyParamImportance[studyId] || null
|
||||
}
|
||||
|
||||
export const useStudyDirections = (
|
||||
studyId: number
|
||||
): StudyDirection[] | null => {
|
||||
const studyDetail = useStudyDetailValue(studyId)
|
||||
const studySummary = useStudySummaryValue(studyId)
|
||||
return studyDetail?.directions || studySummary?.directions || null
|
||||
}
|
||||
|
||||
export const useStudyName = (studyId: number): string | null => {
|
||||
const studyDetail = useStudyDetailValue(studyId)
|
||||
const studySummary = useStudySummaryValue(studyId)
|
||||
return studyDetail?.name || studySummary?.study_name || null
|
||||
}
|
||||
|
||||
Vendored
+5
-3
@@ -21,6 +21,8 @@ type Distribution =
|
||||
| "IntLogUniformDistribution"
|
||||
| "CategoricalDistribution"
|
||||
|
||||
type PageId = "history" | "analytics" | "trialTable" | "trialList" | "note"
|
||||
|
||||
type GraphVisibility = {
|
||||
history: boolean
|
||||
paretoFront: boolean
|
||||
@@ -80,6 +82,7 @@ declare interface Trial {
|
||||
params: TrialParam[]
|
||||
user_attrs: Attribute[]
|
||||
system_attrs: Attribute[]
|
||||
note: Note
|
||||
}
|
||||
|
||||
declare interface StudySummary {
|
||||
@@ -109,7 +112,6 @@ declare interface StudyDetails {
|
||||
[study_id: string]: StudyDetail
|
||||
}
|
||||
|
||||
declare interface ParamImportances {
|
||||
target_name: string
|
||||
param_importances: ParamImportance[]
|
||||
declare interface StudyParamImportance {
|
||||
[study_id: string]: ParamImportance[][]
|
||||
}
|
||||
|
||||
Generated
+2367
-1
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -26,8 +26,11 @@
|
||||
"plotly.js-dist-min": "^2.17.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-markdown": "^8.0.4",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"recoil": "^0.7.6"
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"recoil": "^0.7.6",
|
||||
"remark-gfm": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.3",
|
||||
@@ -38,6 +41,7 @@
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.1",
|
||||
"@typescript-eslint/parser": "^4.26.1",
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
|
||||
@@ -15,7 +15,7 @@ class NoteTestCase(TestCase):
|
||||
("012345", 2),
|
||||
]:
|
||||
with self.subTest(f"with_{dummy_body_str}_{attr_len}"):
|
||||
attrs = note.split_body(dummy_body_str)
|
||||
attrs = note.split_body(dummy_body_str, None)
|
||||
assert len(attrs) == attr_len
|
||||
actual = note.concat_body(attrs)
|
||||
actual = note.concat_body(attrs, None)
|
||||
assert dummy_body_str == actual
|
||||
|
||||
@@ -22,6 +22,10 @@ const trials = [
|
||||
],
|
||||
user_attrs: [],
|
||||
system_attrs: [],
|
||||
note: {
|
||||
body: "",
|
||||
version: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
trial_id: 2,
|
||||
@@ -38,6 +42,10 @@ const trials = [
|
||||
],
|
||||
user_attrs: [],
|
||||
system_attrs: [],
|
||||
note: {
|
||||
body: "",
|
||||
version: 0,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user