CellModules
OptunaWrapper.py
Go to the documentation of this file.
1import sqlite3
2import json
3import inspect
4import textwrap
5import linecache
6import ast
7import base64
8import pickle
9import os
10import sysconfig
11from contextlib import contextmanager
12from typing import Any, Optional, Iterable
13from urllib.parse import unquote
14import matplotlib.pyplot as plt
15import seaborn as sns
16import numpy as np
17import pandas as pd
18
19from .LHSIterator import LHSIterator
20
21
22# ============================================================
23# Helpers
24# ============================================================
25
26def variable_to_pickle64b(value: Any) -> str:
27 data = pickle.dumps(value, protocol=pickle.HIGHEST_PROTOCOL)
28 return base64.b64encode(data).decode("ascii")
29
30
31def pickle64b_to_variable(payload: str) -> Any:
32 data = base64.b64decode(payload.encode("ascii"))
33 return pickle.loads(data)
34
35
36
38 """
39 Return the first 'user' frame (not this lib, not stdlib).
40 Works better with contextmanager wrappers (contextlib).
41 """
42 lib_file = os.path.abspath(__file__) if "__file__" in globals() else None
43
44 stdlib_dir = os.path.abspath(sysconfig.get_paths()["stdlib"])
45 purelib_dir = os.path.abspath(sysconfig.get_paths().get("purelib", ""))
46 platlib_dir = os.path.abspath(sysconfig.get_paths().get("platlib", ""))
47
48 def is_internal_frame(fr) -> bool:
49 filename = fr.f_code.co_filename
50
51 # Cas notebook custom: "Cell 12" -> ce n'est pas un vrai path, on le garde
52 if not os.path.isabs(filename):
53 # ex: "Cell 3", "<ipython-input-...>", "<stdin>"
54 return False
55
56 filename = os.path.abspath(filename)
57
58 # 1) ce fichier de lib
59 if lib_file is not None and filename == lib_file:
60 return True
61
62 # 2) stdlib (contextlib, inspect, etc.)
63 if filename.startswith(stdlib_dir + os.sep):
64 return True
65
66 # 3) site-packages (optionnel, mais souvent utile)
67 if purelib_dir and filename.startswith(purelib_dir + os.sep):
68 return True
69 if platlib_dir and filename.startswith(platlib_dir + os.sep):
70 return True
71
72 return False
73
74 fr = inspect.currentframe()
75 try:
76 fr = fr.f_back
77 while fr is not None:
78 # debug éventuel:
79 # print("STACK:", fr.f_code.co_filename, fr.f_code.co_name)
80
81 if not is_internal_frame(fr):
82 return fr
83
84 fr = fr.f_back
85
86 return None
87 finally:
88 del fr
89
90
93 if fr is None:
94 return {}, {}
95 return fr.f_globals, fr.f_locals
96
97
98def _get_function_source(fn) -> str:
99 """
100 Robust-ish getsource for normal .py + custom notebooks if linecache is populated.
101 """
102 try:
103 return textwrap.dedent(inspect.getsource(fn))
104 except Exception:
105 code = getattr(fn, "__code__", None)
106 if code is None:
107 raise
108
109 filename = code.co_filename
110 lines = linecache.getlines(filename)
111 if not lines:
112 raise
113
114 src = "".join(lines)
115 tree = ast.parse(src)
116
117 # Try exact function match by name + line number
118 target_name = fn.__name__
119 target_lineno = getattr(code, "co_firstlineno", None)
120
121 for node in tree.body:
122 if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == target_name:
123 if target_lineno is None or node.lineno == target_lineno:
124 if hasattr(node, "end_lineno") and node.end_lineno is not None:
125 return textwrap.dedent("".join(lines[node.lineno - 1: node.end_lineno]))
126 return textwrap.dedent("".join(lines[node.lineno - 1:]))
127
128 raise
129
130
131# ============================================================
132# Optuna Archive
133# ============================================================
134
136 """
137 Lightweight helper to standardize what is stored in Optuna study/trial user_attrs.
138
139 Main idea:
140 - Typed helpers for trial outputs (timeseries)
141 - Explicit function registration (suggest/run/eval)
142 - Optional context capture via `with archiver.register(): ...`
143 to archive helper functions + runtime objects automatically
144 into study_user_attributes['backupContext'].
145 """
146
147 KEY_ENTRY_SUGGEST = "entrypoint:suggest"
148 KEY_ENTRY_RUN = "entrypoint:run"
149 KEY_ENTRY_EVAL = "entrypoint:eval"
150 KEY_CONTEXT = "backupContext"
151
152 # contexte partagé unique par DB (pas de study_map)
153 CONTEXT_ID = "__default__"
154
155 def __init__(self, study=None):
156 self.study = None
157 self.backupContext: dict[str, dict[str, Any]] = {}
158
159 # cache des entrypoints tant qu'on n'a pas de study
160 self._entrypoints_cache: dict[str, str] = {}
161
162 # connexion sqlite custom (ouverte après set_study)
163 self._db_path: Optional[str] = None
164 self._con_custom: Optional[sqlite3.Connection] = None
165
166 if study is not None:
167 self.set_study(study)
168
169 # ---------------------------
170 # Study attach / DB init / flush
171 # ---------------------------
172 def set_study(self, study):
173 self.study = study
175
176 # ouvre connexion vers la même DB SQLite qu'Optuna
177 if self._con_custom is not None:
178 try:
179 self._con_custom.close()
180 except Exception:
181 pass
182 self._con_custom = sqlite3.connect(self._db_path)
183
187 return self
188
190 if self._con_custom is None:
191 raise RuntimeError(
192 "No custom DB connection yet. Call set_study(study) first."
193 )
194
195
196 def _require_study(self):
197 if self.study is None:
198 raise RuntimeError("OptunaArchive.study is None. Attach a study first (OptunaArchive(study) or set_study()).")
199
200
202 self._require_custom_db()
203 cur = self._con_custom.cursor()
204
205 cur.execute("""
206 CREATE TABLE IF NOT EXISTS isicell_context (
207 context_id TEXT NOT NULL,
208 key TEXT NOT NULL,
209 mode TEXT NOT NULL,
210 payload TEXT NOT NULL,
211 PRIMARY KEY (context_id, key)
212 );
213 """)
214
215 cur.execute("""
216 CREATE TABLE IF NOT EXISTS isicell_context_entrypoints (
217 context_id TEXT NOT NULL,
218 kind TEXT NOT NULL, -- suggest | run | eval
219 name TEXT NOT NULL,
220 PRIMARY KEY (context_id, kind)
221 );
222 """)
223
224 self._con_custom.commit()
225
227 """
228 Flush in-memory backupContext to custom table.
229 Upsert behavior (safe if called multiple times).
230 """
231 if self._con_custom is None or not self.backupContext:
232 return
233
235 cur = self._con_custom.cursor()
236
237 for name, entry in self.backupContext.items():
238 mode = entry.get("mode")
239 payload = entry.get("payload")
240
241 # payload must be text in SQL table
242 if mode == "json":
243 payload_txt = json.dumps(payload)
244 else:
245 # mode source / pickle64b already string-like
246 payload_txt = str(payload)
247
248 cur.execute("""
249 INSERT INTO isicell_context (context_id, key, mode, payload)
250 VALUES (?, ?, ?, ?)
251 ON CONFLICT(context_id, key) DO UPDATE SET
252 mode=excluded.mode,
253 payload=excluded.payload;
254 """, (self.CONTEXT_ID, name, mode, payload_txt))
255
256 self._con_custom.commit()
257
259 """
260 Flush cached entrypoints to custom table.
261 """
262 if self._con_custom is None or not self._entrypoints_cache:
263 return
264
266 cur = self._con_custom.cursor()
267
268 for kind, name in self._entrypoints_cache.items():
269 cur.execute("""
270 INSERT INTO isicell_context_entrypoints (context_id, kind, name)
271 VALUES (?, ?, ?)
272 ON CONFLICT(context_id, kind) DO UPDATE SET
273 name=excluded.name;
274 """, (self.CONTEXT_ID, kind, name))
275
276 self._con_custom.commit()
277
278 # ---------------------------
279 # Serialization helpers
280 # ---------------------------
281 @staticmethod
282 def _to_jsonable(x: Any) -> Any:
283 if isinstance(x, np.ndarray):
284 return x.tolist()
285 if isinstance(x, (np.integer, np.floating)):
286 return x.item()
287 if isinstance(x, pd.Series):
288 return {
289 "__kind__": "series",
290 "name": x.name,
291 "value": x.to_dict()
292 }
293 if isinstance(x, pd.DataFrame):
294 return {
295 "__kind__": "dataframe",
296 "orient": "split",
297 "value": x.to_dict("split")
298 }
299 if isinstance(x, dict):
300 return {k: OptunaArchive._to_jsonable(v) for k, v in x.items()}
301 if isinstance(x, (list, tuple)):
302 return [OptunaArchive._to_jsonable(v) for v in x]
303 # Let plain python scalars/strings/bool/None pass
304 return x
305
306 def _encode_context_value(self, value: Any) -> dict[str, Any]:
307 """
308 Context encoding policy:
309 - functions/classes/callables -> source code (mode='source') if possible
310 - JSON-serializable (after _to_jsonable) -> mode='json'
311 - fallback -> pickle64b
312 """
313 if callable(value):
314 src = _get_function_source(value)
315 return {"mode": "source", "payload": src}
316
317 try:
318 j = self._to_jsonable(value)
319 json.dumps(j) # validate serializable
320 return {"mode": "json", "payload": j}
321 except Exception:
322 return {"mode": "pickle64b", "payload": variable_to_pickle64b(value)}
323
324 # ---------------------------
325 # Context manager capture
326 # ---------------------------
327 @contextmanager
328 def register(self):
329 """
330 Capture newly created symbols in caller scope and store them in backupContext.
331
332 Example
333 -------
334 archiver = OptunaArchive(study)
335 with archiver.register():
336 data = {...}
337 def dataCatcherOnStep(...): ...
338 def run_simu(...): ...
339 def optim(trial): ...
340 # all new symbols now archived in study_user_attributes['backupContext']
341 """
343 before_g = set(g0.keys())
344 before_l = set(l0.keys())
345 try:
346 yield self
347 finally:
349
350 new_names = [k for k in l1.keys() if k not in before_l]
351 new_names += [k for k in g1.keys() if k not in before_g and k not in new_names]
352
353 for name in new_names:
354 if name.startswith("__"):
355 continue
356 value = l1[name] if name in l1 else g1[name]
357 try:
358 self.backupContext[name] = self._encode_context_value(value)
359 except Exception as e:
360 raise RuntimeError(f"Failed to archive symbol '{name}'.\nerror: {e}") from e
361
362 # si study déjà attachée, flush immédiat; sinon ça reste en cache
364
365 # ---------------------------
366 # Manual context registration (optional)
367 # ---------------------------
368 def context_add(self, name: str, value: Any):
369 self.backupContext[name] = self._encode_context_value(value)
371
372 def context_add_many(self, mapping: dict[str, Any]):
373 for k, v in mapping.items():
374 self.context_add(k, v)
375
376 def _extract_sqlite_path_from_study(self, study) -> str:
377 """
378 Extract sqlite file path from an Optuna study backed by RDBStorage(sqlite),
379 even if wrapped by _CachedStorage.
380 """
381 storage = getattr(study, "_storage", None)
382 if storage is None:
383 raise RuntimeError("Study has no _storage attribute.")
384
385 # Unwrap common Optuna wrappers (_CachedStorage, etc.)
386 visited = set()
387 while storage is not None and id(storage) not in visited:
388 visited.add(id(storage))
389
390 engine = getattr(storage, "engine", None)
391 if engine is not None:
392 url = str(engine.url)
393 if not url.startswith("sqlite:///"):
394 raise RuntimeError(f"Only sqlite storage is supported. Got storage URL: {url}")
395
396 path = url[len("sqlite:///"):] # keep leading slash if absolute path
397 path = unquote(path)
398
399 if not os.path.isabs(path):
400 path = os.path.abspath(path)
401 return path
402
403 # Try common wrapper attributes used by Optuna versions
404 if hasattr(storage, "_backend"):
405 storage = storage._backend
406 continue
407 if hasattr(storage, "_storage"):
408 storage = storage._storage
409 continue
410
411 break
412
413 raise RuntimeError(
414 "Could not access underlying RDBStorage engine from study._storage "
415 "(possibly an unsupported Optuna storage wrapper/version)."
416 )
417
418 def _set_entrypoint(self, key: str, name: str):
419 """
420 API inchangée:
421 - vérifie que le symbole existe dans backupContext et est source-backed
422 - stocke en cache + flush si possible
423 """
424 if name not in self.backupContext:
425 raise KeyError(
426 f"'{name}' not found in backupContext. "
427 "Define it inside `with archiver.register(): ...` first."
428 )
429
430 entry = self.backupContext[name]
431 if entry.get("mode") != "source":
432 raise TypeError(
433 f"Entrypoint '{name}' must be a source-backed symbol, "
434 f"got mode='{entry.get('mode')}'."
435 )
436
437 # map old keys -> compact kinds in custom table
438 if key == self.KEY_ENTRY_SUGGEST:
439 kind = "suggest"
440 elif key == self.KEY_ENTRY_RUN:
441 kind = "run"
442 elif key == self.KEY_ENTRY_EVAL:
443 kind = "eval"
444 else:
445 raise KeyError(f"Unknown entrypoint key: {key}")
446
447 self._entrypoints_cache[kind] = name
449
450 def set_suggest_entrypoint(self, name: str):
451 self._set_entrypoint(self.KEY_ENTRY_SUGGEST, name)
452
453 def set_run_entrypoint(self, name: str):
454 self._set_entrypoint(self.KEY_ENTRY_RUN, name)
455
456 def set_eval_entrypoint(self, name: str):
457 self._set_entrypoint(self.KEY_ENTRY_EVAL, name)
458
459 # ---------------------------
460 # Generic study/trial data
461 # ---------------------------
462 def study_add_data(self, key: str, value: Any):
463 self._require_study()
464 self.study.set_user_attr(key, self._to_jsonable(value))
465
466 @staticmethod
467 def trial_add_data(trial, key: str, value: Any):
468 trial.set_user_attr(key, OptunaArchive._to_jsonable(value))
469
470 # ---------------------------
471 # Axes helpers (for timeseries / vectors)
472 # ---------------------------
473 def study_add_axis(self, name: str, values: Any):
474 self._require_study()
475 self.study.set_user_attr(f"axis:{name}", self._to_jsonable(values))
476
477 # ---------------------------
478 # Typed trial helpers
479 # ---------------------------
481 self,
482 trial,
483 key: str,
484 values: Any,
485 axes: list[str],
486 columns: Optional[list[str]] = None,
487 value_col: str = "value",
488 layout: str = "aligned",
489 ):
490 """
491 Store trial-level flattened values and a study-level spec.
492
493 layout='product' : cartesian product of axes
494 layout='aligned' : zipped/aligned axes (same length as values)
495 """
496 self._require_study()
497
498 if not isinstance(axes, (list, tuple)) or len(axes) < 1:
499 raise ValueError("axes must be a non-empty list of axis names.")
500 if columns is None:
501 columns = list(axes)
502 if len(columns) != len(axes):
503 raise ValueError("columns and axes must have the same length.")
504 if layout not in ("product", "aligned"):
505 raise ValueError("layout must be 'product' or 'aligned'.")
506
507 for ax in axes:
508 if f"axis:{ax}" not in self.study.user_attrs:
509 raise KeyError(f"Missing study axis 'axis:{ax}'. Call study_add_axis('{ax}', ...) first.")
510
511 spec_key = f"timeseries_spec:{key}"
512 spec = {
513 "axes": list(axes),
514 "columns": list(columns),
515 "value_col": value_col,
516 "layout": layout,
517 }
518
519 existing = self.study.user_attrs.get(spec_key, None)
520 if existing is None:
521 self.study.set_user_attr(spec_key, spec)
522 elif existing != spec:
523 raise ValueError(f"Incompatible timeseries spec for key '{key}'. Existing={existing}, New={spec}")
524
525 # Always store trial payload
526 trial.set_user_attr(key, self._to_jsonable(values))
527
528 # ---------------------------
529 # Validation
530 # ---------------------------
531 def check_minimal(self, require_run_function: bool = True):
532 missing = []
533
534 if "suggest" not in self._entrypoints_cache:
535 missing.append("suggest entrypoint")
536 if require_run_function and "run" not in self._entrypoints_cache:
537 missing.append("run entrypoint")
538
539 if missing:
540 raise RuntimeError(
541 f"Missing required archive entrypoints: {missing}. "
542 "Use set_suggest_entrypoint/set_run_entrypoint before optimize()."
543 )
544
545 # Si study déjà attachée, on vérifie aussi que tout est bien flush en DB custom
546 if self._con_custom is not None:
547 cur = self._con_custom.cursor()
548 rows = cur.execute("""
549 SELECT kind, name
550 FROM isicell_context_entrypoints
551 WHERE context_id = ?;
552 """, (self.CONTEXT_ID,)).fetchall()
553 kinds_in_db = {k for k, _ in rows}
554
555 if "suggest" not in kinds_in_db:
556 raise RuntimeError("Missing 'suggest' entrypoint in isicell_context_entrypoints.")
557 if require_run_function and "run" not in kinds_in_db:
558 raise RuntimeError("Missing 'run' entrypoint in isicell_context_entrypoints.")
559
560
561# ============================================================
562# Optuna DB reader / replay
563# ============================================================
564
566 """
567 Read-only Optuna SQLite helper (fast SQL) + archived function loading (exec) + replay-ready helpers.
568
569 Context and entrypoints are read from custom tables:
570 - isicell_context
571 - isicell_context_entrypoints
572
573 Study-specific metadata (axes, timeseries specs, etc.) remains in study_user_attributes.
574 """
575
576 CONTEXT_ID = "__default__"
577
578 def __init__(self, db_path: str, readonly: bool = True):
579 self.db_path = db_path
580 if readonly:
581 self.con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
582 else:
583 self.con = sqlite3.connect(db_path)
584
585 self.suggest_fn = None
586 self.run_fn = None
587 self.eval_fn = None
588
589 # charge le contexte global si tables présentes
590 if self._has_isicell_tables():
591 self.load_context()
592 elif self.studies():
593 # fallback legacy possible (si ancienne DB)
594 # self.load_context_legacy(self.studies()[0])
595 pass
596
597 def close(self):
598 self.con.close()
599
600 # ============================================================
601 # Basic metadata
602 # ============================================================
603 def studies(self) -> list[str]:
604 rows = self.con.execute("SELECT study_name FROM studies ORDER BY study_name;").fetchall()
605 return [r[0] for r in rows]
606
607 def trial_to_study(self, trial_id: int) -> str:
608 row = self.con.execute(
609 """
610 SELECT studies.study_name
611 FROM trials
612 INNER JOIN studies ON studies.study_id = trials.study_id
613 WHERE trials.trial_id = ?;
614 """,
615 (int(trial_id),),
616 ).fetchone()
617 if row is None:
618 raise KeyError(f"trial_id {trial_id} not found")
619 return row[0]
620
621 def study_attrs(self, study_name: str) -> dict[str, Any]:
622 rows = self.con.execute(
623 """
624 SELECT key, value_json
625 FROM study_user_attributes
626 INNER JOIN studies ON studies.study_id = study_user_attributes.study_id
627 WHERE studies.study_name = ?;
628 """,
629 (study_name,),
630 ).fetchall()
631 return {k: json.loads(v) for k, v in rows}
632
633 def trial_attrs(self, study_name: str, key: Optional[str] = None) -> pd.DataFrame:
634 if key is None:
635 df = pd.read_sql_query(
636 """
637 SELECT studies.study_name, trial_user_attributes.trial_id, trial_user_attributes.key, trial_user_attributes.value_json
638 FROM trial_user_attributes
639 INNER JOIN trials ON trial_user_attributes.trial_id = trials.trial_id
640 INNER JOIN studies ON studies.study_id = trials.study_id
641 WHERE trials.state="COMPLETE" AND studies.study_name = ?;
642 """,
643 self.con,
644 params=(study_name,),
645 )
646 else:
647 df = pd.read_sql_query(
648 """
649 SELECT studies.study_name, trial_user_attributes.trial_id, trial_user_attributes.key, trial_user_attributes.value_json
650 FROM trial_user_attributes
651 INNER JOIN trials ON trial_user_attributes.trial_id = trials.trial_id
652 INNER JOIN studies ON studies.study_id = trials.study_id
653 WHERE trials.state="COMPLETE" AND studies.study_name = ? AND trial_user_attributes.key = ?;
654 """,
655 self.con,
656 params=(study_name, key),
657 )
658
659 if df.empty:
660 return pd.DataFrame(columns=["study_name", "trial_id", "key", "value"])
661
662 df = df.rename(columns={"value_json": "value"})
663 df["value"] = df["value"].map(json.loads)
664 return df
665
666 # ============================================================
667 # Fitness / params (fast SQL)
668 # ============================================================
669 def fitness_long(self, study_name: Optional[str] = None) -> pd.DataFrame:
670 if study_name is None:
671 return pd.read_sql_query(
672 """
673 SELECT studies.study_name, trial_values.trial_id, trial_values.objective, trial_values.value
674 FROM trial_values
675 INNER JOIN trials ON trial_values.trial_id = trials.trial_id
676 INNER JOIN studies ON studies.study_id = trials.study_id
677 WHERE trials.state="COMPLETE";
678 """,
679 self.con,
680 )
681
682 return pd.read_sql_query(
683 """
684 SELECT studies.study_name, trial_values.trial_id, trial_values.objective, trial_values.value
685 FROM trial_values
686 INNER JOIN trials ON trial_values.trial_id = trials.trial_id
687 INNER JOIN studies ON studies.study_id = trials.study_id
688 WHERE trials.state="COMPLETE" AND studies.study_name = ?;
689 """,
690 self.con,
691 params=(study_name,),
692 )
693
694 def fitness_wide(self, study_name: Optional[str] = None) -> pd.DataFrame:
695 res = self.fitness_long(study_name=study_name)
696 if res.empty:
697 return res
698
699 wide = res.pivot(index=["study_name", "trial_id"], columns="objective", values="value").reset_index()
700 wide["trial_num"] = wide.groupby("study_name")["trial_id"].transform(lambda s: s - s.min())
701
702 if 0 in wide.columns:
703 wide = wide.sort_values(["study_name", "trial_num"]).reset_index(drop=True)
704 wide["best"] = wide.groupby("study_name")[0].cummin()
705
706 return wide
707
708 def params(self, study_name: Optional[str] = None) -> pd.DataFrame:
709 if study_name is None:
710 res = pd.read_sql_query(
711 """
712 SELECT studies.study_name, trial_params.trial_id, trial_params.param_name, trial_params.param_value
713 FROM trial_params
714 INNER JOIN trials ON trial_params.trial_id = trials.trial_id
715 INNER JOIN studies ON studies.study_id = trials.study_id
716 WHERE trials.state="COMPLETE";
717 """,
718 self.con,
719 )
720 else:
721 res = pd.read_sql_query(
722 """
723 SELECT studies.study_name, trial_params.trial_id, trial_params.param_name, trial_params.param_value
724 FROM trial_params
725 INNER JOIN trials ON trial_params.trial_id = trials.trial_id
726 INNER JOIN studies ON studies.study_id = trials.study_id
727 WHERE trials.state="COMPLETE" AND studies.study_name = ?;
728 """,
729 self.con,
730 params=(study_name,),
731 )
732
733 if res.empty:
734 return pd.DataFrame()
735
736 return res.pivot(index=["study_name", "trial_id"], columns="param_name", values="param_value")
737
738 # ============================================================
739 # Timeseries helper
740 # ============================================================
741 def _normalize_ids(self, x, cast=int, name="ids"):
742 """
743 Normalize None | scalar | iterable -> None | list[cast(x)].
744 Strings are treated as scalars (not iterables).
745 """
746 if x is None:
747 return None
748
749 # scalar str/int/etc.
750 if isinstance(x, (str, bytes)):
751 return [cast(x)]
752 if not isinstance(x, Iterable):
753 return [cast(x)]
754
755 # iterable
756 vals = [cast(v) for v in x]
757 return vals
758
759
760 def _sql_in_clause(self, values):
761 """
762 Returns ('(?,?,?)', [..]) for SQL IN clauses.
763 values must be a non-empty list.
764 """
765 if values is None or len(values) == 0:
766 raise ValueError("values must be a non-empty list")
767 return "(" + ",".join(["?"] * len(values)) + ")", values
768
770 self,
771 key: str,
772 study_name=None, # None | str | Iterable[str]
773 trial_ids=None, # None | int | Iterable[int]
774 ) -> pd.DataFrame:
775 study_names = self._normalize_ids(study_name, cast=str, name="study_name")
776 trial_ids = self._normalize_ids(trial_ids, cast=int, name="trial_ids")
777
778 if study_names is None:
779 if trial_ids is None:
780 # cas "tout"
781 study_names = self.studies()
782 else:
783 # déduire les studies à partir des trial_ids
784 if len(trial_ids) == 0:
785 return pd.DataFrame(columns=["study_name", "trial_id", "value"])
786 in_clause, in_vals = self._sql_in_clause(trial_ids)
787 rows = self.con.execute(
788 f"""
789 SELECT DISTINCT studies.study_name
790 FROM trials
791 INNER JOIN studies ON studies.study_id = trials.study_id
792 WHERE trials.trial_id IN {in_clause}
793 ORDER BY studies.study_name;
794 """,
795 in_vals,
796 ).fetchall()
797 study_names = [r[0] for r in rows]
798
799 if not study_names:
800 return pd.DataFrame(columns=["study_name", "trial_id", "value"])
801
802 # ------------------------------------------------------------
803 # Lire et valider les specs (une par study)
804 # ------------------------------------------------------------
805 specs_by_study = {}
806 base_spec = None
807 base_cols = None
808 base_value_col = None
809
810 for sn in study_names:
811 attrs = self.study_attrs(sn)
812 spec_key = f"timeseries_spec:{key}"
813 if spec_key not in attrs:
814 raise KeyError(f"Missing study_user_attributes['{spec_key}'] for study '{sn}'")
815
816 spec = attrs[spec_key]
817 axis_names = spec["axes"]
818 col_names = spec["columns"]
819 value_col = spec.get("value_col", "value")
820 layout = spec.get("layout", "product")
821
822 axis_values = []
823 for ax in axis_names:
824 k = f"axis:{ax}"
825 if k not in attrs:
826 raise KeyError(f"Missing study_user_attributes['{k}'] in study '{sn}'")
827 axis_values.append(attrs[k])
828
829 # Spec complète (incluant axes values) pour compatibilité inter-studies
830 spec_full = {
831 "axes": axis_names,
832 "columns": col_names,
833 "value_col": value_col,
834 "layout": layout,
835 "axis_values": axis_values,
836 }
837
838 if base_spec is None:
839 base_spec = spec_full
840 base_cols = list(col_names)
841 base_value_col = value_col
842 else:
843 # vérifier compatibilité stricte
844 if spec_full != base_spec:
845 raise ValueError(
846 f"Timeseries spec mismatch for key '{key}' between studies. "
847 f"Study '{sn}' has a different spec/axes."
848 )
849
850 specs_by_study[sn] = spec_full
851
852 # ------------------------------------------------------------
853 # Charger les payloads trial_user_attributes (toutes studies demandées)
854 # ------------------------------------------------------------
855 sql = """
856 SELECT studies.study_name, trial_user_attributes.trial_id, trial_user_attributes.value_json
857 FROM trial_user_attributes
858 INNER JOIN trials ON trial_user_attributes.trial_id = trials.trial_id
859 INNER JOIN studies ON studies.study_id = trials.study_id
860 WHERE trials.state="COMPLETE"
861 AND trial_user_attributes.key = ?
862 """
863 params = [key]
864
865 if study_names is not None:
866 in_clause, in_vals = self._sql_in_clause(study_names)
867 sql += f" AND studies.study_name IN {in_clause}"
868 params.extend(in_vals)
869
870 if trial_ids is not None:
871 if len(trial_ids) == 0:
872 return pd.DataFrame(columns=["study_name", "trial_id", *base_cols, base_value_col])
873 in_clause, in_vals = self._sql_in_clause(trial_ids)
874 sql += f" AND trial_user_attributes.trial_id IN {in_clause}"
875 params.extend(in_vals)
876
877 raw = pd.read_sql_query(sql, self.con, params=params)
878
879 if raw.empty:
880 return pd.DataFrame(columns=["study_name", "trial_id", *base_cols, base_value_col])
881
882 # ------------------------------------------------------------
883 # Expand per-study (pour réutiliser axes/spec de chaque study)
884 # ------------------------------------------------------------
885 out_parts = []
886
887 for sn, raw_sn in raw.groupby("study_name", sort=False):
888 spec = specs_by_study[sn]
889 axis_names = spec["axes"]
890 col_names = spec["columns"]
891 value_col = spec["value_col"]
892 layout = spec["layout"]
893 axis_values = spec["axis_values"]
894
895 arr = np.asarray([json.loads(v) for v in raw_sn["value_json"]], dtype=float)
896
897 if layout == "aligned":
898 n = arr.shape[1]
899 for ax, vals in zip(axis_names, axis_values):
900 if len(vals) != n:
901 raise ValueError(
902 f"Aligned layout mismatch for axis '{ax}' in study '{sn}': {len(vals)} != {n}"
903 )
904
905 rows = []
906 for i, (_, tid) in enumerate(raw_sn[["study_name", "trial_id"]].to_numpy()):
907 d = pd.DataFrame({"study_name": sn, "trial_id": tid, value_col: arr[i]})
908 for cname, vals in zip(col_names, axis_values):
909 d[cname] = vals
910 rows.append(d)
911 out_parts.append(pd.concat(rows, ignore_index=True))
912 continue
913
914 if layout == "product":
915 if len(axis_values) == 1:
916 cols = pd.Index(axis_values[0], name=col_names[0])
917 else:
918 cols = pd.MultiIndex.from_product(axis_values, names=col_names)
919
920 if arr.shape[1] != len(cols):
921 raise ValueError(
922 f"Timeseries size mismatch for key '{key}' in study '{sn}': "
923 f"payload length={arr.shape[1]} expected={len(cols)}"
924 )
925
926 wide = pd.DataFrame(
927 arr,
928 index=pd.MultiIndex.from_frame(raw_sn[["study_name", "trial_id"]]),
929 columns=cols,
930 )
931 out_parts.append(
932 wide.reset_index().melt(
933 id_vars=["study_name", "trial_id"],
934 var_name=col_names if len(col_names) > 1 else col_names[0],
935 value_name=value_col,
936 )
937 )
938 continue
939
940 raise ValueError(f"Unknown timeseries layout '{layout}' in study '{sn}'")
941
942 if not out_parts:
943 return pd.DataFrame(columns=["study_name", "trial_id", *base_cols, base_value_col])
944
945 return pd.concat(out_parts, ignore_index=True)
946
948 self,
949 key: str,
950 study_name=None, # None | str | Iterable[str]
951 trial_ids=None, # None | int | Iterable[int]
952 raw=False,
953 sep='.'
954 ) -> pd.DataFrame:
955 if not raw:
956 df = self.get_trial_data(key=key, study_name=study_name, trial_ids=trial_ids,raw=True)
957 if df.empty:
958 return df
959
960 # All dict payloads -> normalize
961 is_dict = df["value"].map(lambda x: isinstance(x, dict))
962 if is_dict.all():
963 expanded = pd.json_normalize(df["value"], sep=sep)
964 expanded.index = df.index
965 out = pd.concat([df[["study_name", "trial_id"]], expanded], axis=1)
966 return out
967
968 # Mixed payloads -> best effort:
969 # normalize dict rows, keep others in "value"
970 if is_dict.any():
971 out = df[["study_name", "trial_id"]].copy()
972 out["value"] = df["value"]
973
974 dict_rows = df[is_dict]
975 expanded = pd.json_normalize(dict_rows["value"], sep=sep)
976 expanded.index = dict_rows.index
977
978 out = pd.concat([out, expanded], axis=1)
979 return out
980
981 # No dict payloads
982 return df
983
984 study_names = self._normalize_ids(study_name, cast=str, name="study_name")
985 trial_ids = self._normalize_ids(trial_ids, cast=int, name="trial_ids")
986
987 sql = """
988 SELECT studies.study_name, trial_user_attributes.trial_id, trial_user_attributes.value_json
989 FROM trial_user_attributes
990 INNER JOIN trials ON trial_user_attributes.trial_id = trials.trial_id
991 INNER JOIN studies ON studies.study_id = trials.study_id
992 WHERE trials.state="COMPLETE"
993 AND trial_user_attributes.key = ?
994 """
995 params = [key]
996
997 if study_names is not None:
998 if len(study_names) == 0:
999 return pd.DataFrame(columns=["study_name", "trial_id", "value"])
1000 in_clause, in_vals = self._sql_in_clause(study_names)
1001 sql += f" AND studies.study_name IN {in_clause}"
1002 params.extend(in_vals)
1003
1004 if trial_ids is not None:
1005 if len(trial_ids) == 0:
1006 return pd.DataFrame(columns=["study_name", "trial_id", "value"])
1007 in_clause, in_vals = self._sql_in_clause(trial_ids)
1008 sql += f" AND trial_user_attributes.trial_id IN {in_clause}"
1009 params.extend(in_vals)
1010
1011 df = pd.read_sql_query(sql, self.con, params=params)
1012
1013 if df.empty:
1014 return pd.DataFrame(columns=["study_name", "trial_id", "value"])
1015
1016 df = df.rename(columns={"value_json": "value"})
1017 df["value"] = df["value"].map(json.loads)
1018 return df
1019 # ============================================================
1020 # isicell custom context tables
1021 # ============================================================
1022 def _has_isicell_tables(self) -> bool:
1023 rows = self.con.execute(
1024 """
1025 SELECT name
1026 FROM sqlite_master
1027 WHERE type='table' AND name IN ('isicell_context', 'isicell_context_entrypoints');
1028 """
1029 ).fetchall()
1030 names = {r[0] for r in rows}
1031 return "isicell_context" in names and "isicell_context_entrypoints" in names
1032
1033 def _read_isicell_context_rows(self) -> list[tuple[str, str, str]]:
1034 """
1035 Returns rows: (key, mode, payload) for CONTEXT_ID.
1036 """
1037 if not self._has_isicell_tables():
1038 return []
1039
1040 return self.con.execute(
1041 """
1042 SELECT key, mode, payload
1043 FROM isicell_context
1044 WHERE context_id = ?
1045 ORDER BY key;
1046 """,
1047 (self.CONTEXT_ID,),
1048 ).fetchall()
1049
1050 def _read_isicell_entrypoints(self) -> dict[str, str]:
1051 """
1052 Returns {"suggest": "optim", "run": "run_simu", ...}
1053 """
1054 if not self._has_isicell_tables():
1055 return {}
1056
1057 rows = self.con.execute(
1058 """
1059 SELECT kind, name
1060 FROM isicell_context_entrypoints
1061 WHERE context_id = ?;
1062 """,
1063 (self.CONTEXT_ID,),
1064 ).fetchall()
1065 return {k: n for k, n in rows}
1066
1067 # ============================================================
1068 # Context decode / load
1069 # ============================================================
1070 @staticmethod
1071 def _decode_json_runtime_value(v: Any) -> Any:
1072 if isinstance(v, dict) and v.get("__kind__") == "dataframe" and v.get("orient") == "split":
1073 vv = v["value"]
1074 return pd.DataFrame(data=vv["data"], index=vv["index"], columns=vv["columns"])
1075 if isinstance(v, dict) and v.get("__kind__") == "series":
1076 s = pd.Series(v["value"])
1077 s.name = v.get("name")
1078 return s
1079 return v
1080
1081 def context_value(self, name: str) -> Any:
1082 """
1083 Return a data object from the shared isicell context (json/pickle only).
1084 """
1085 rows = self._read_isicell_context_rows()
1086 if not rows:
1087 raise RuntimeError("No isicell_context table (or empty context) in this DB.")
1088
1089 for key, mode, payload_txt in rows:
1090 if key != name:
1091 continue
1092
1093 if mode == "json":
1094 return self._decode_json_runtime_value(json.loads(payload_txt))
1095 if mode == "pickle64b":
1096 return pickle64b_to_variable(payload_txt)
1097
1098 raise TypeError(f"'{name}' is source-backed (mode='source'), not a data object")
1099
1100 raise KeyError(f"'{name}' not found in isicell_context")
1101
1102 def load_context(self) -> dict[str, Any]:
1103 """
1104 Build exec namespace from shared isicell_context + caller namespace.
1105 Source-backed symbols are exec'ed into the same namespace.
1106 Entrypoints are resolved from isicell_context_entrypoints.
1107 """
1109
1110 rows = self._read_isicell_context_rows()
1111 if not rows:
1112 return
1113
1114 # 1) data first
1115 for key, mode, payload_txt in rows:
1116 if mode == "json":
1117 g[key] = self._decode_json_runtime_value(json.loads(payload_txt))
1118 elif mode == "pickle64b":
1119 g[key] = pickle64b_to_variable(payload_txt)
1120
1121 # 2) source next
1122 for key, mode, payload_txt in rows:
1123 if mode == "source":
1124 exec(payload_txt, g, g)
1125
1126 # 3) entrypoints
1127 eps = self._read_isicell_entrypoints()
1128 if "suggest" in eps and eps["suggest"] in g:
1129 self.suggest_fn = g[eps["suggest"]]
1130 if "run" in eps and eps["run"] in g:
1131 self.run_fn = g[eps["run"]]
1132 if "eval" in eps and eps["eval"] in g:
1133 self.eval_fn = g[eps["eval"]]
1134
1135 # ============================================================
1136 # Trial params -> structured params
1137 # ============================================================
1138 @staticmethod
1139 def _coerce_param_value(x: Any) -> Any:
1140 if x is None or isinstance(x, (int, float, bool)):
1141 return x
1142
1143 s = str(x)
1144 if s == "True":
1145 return True
1146 if s == "False":
1147 return False
1148
1149 try:
1150 if any(ch in s for ch in [".", "e", "E"]):
1151 return float(s)
1152 return int(s)
1153 except Exception:
1154 pass
1155
1156 try:
1157 return json.loads(s)
1158 except Exception:
1159 return s
1160
1161 def get_trial_params_flat(self, trial_id: int) -> dict[str, Any]:
1162 df = pd.read_sql_query(
1163 """
1164 SELECT trial_params.param_name, trial_params.param_value
1165 FROM trial_params
1166 INNER JOIN trials ON trial_params.trial_id = trials.trial_id
1167 WHERE trials.state="COMPLETE"
1168 AND trial_params.trial_id = ?;
1169 """,
1170 self.con,
1171 params=(int(trial_id),),
1172 )
1173 return {r["param_name"]: self._coerce_param_value(r["param_value"]) for _, r in df.iterrows()}
1174
1175 def build_params(self, trial_id: int):
1176 """
1177 Rebuild structured params from flat optuna params using archived suggest_fn.
1178 Handles categorical params stored as category indices.
1179 """
1180 if self.suggest_fn is None:
1181 self.load_context()
1182
1183 if self.suggest_fn is None:
1184 raise RuntimeError("No suggest entrypoint found in isicell_context_entrypoints.")
1185
1186 flat = self.get_trial_params_flat(trial_id=trial_id)
1187
1188 bounds = LHSIterator.getBoundaries(self.suggest_fn)
1189 for p, info in bounds.items():
1190 if info.get("type") == "categorical" and p in flat:
1191 try:
1192 flat[p] = info["choices"][int(flat[p])]
1193 except Exception:
1194 # leave as-is if already decoded
1195 pass
1196
1197 fake_trial = LHSIterator.Trial({})
1198 fake_trial.params = flat
1199 fake_trial.modeSuggest = True
1200 return self.suggest_fn(fake_trial)
1201
1203 self,
1204 fitness_threshold: float = 1500,
1205 objective: int = 0,
1206 study_name=None, # None | str | Iterable[str]
1207 trial_ids=None, # None | int | Iterable[int]
1208 figsize_width: float = 11,
1209 row_height: float = 0.42,
1210 min_fig_height: float = 3.0,
1211 label_fontsize: int = 8,
1212 min_cat_label_width: float = 0.08,
1213 max_cat_label_len: int = 14,
1214 ):
1215 """
1216 Plot normalized distributions of selected trials' parameters:
1217 - numeric params: violin plots on x in [0, 1]
1218 - categorical params: stacked horizontal bars of frequencies on x in [0, 1]
1219
1220 Selection logic:
1221 - keep completed trials with fitness(objective) < fitness_threshold
1222 - optional filter by study_name and/or trial_ids
1223
1224 Notes
1225 -----
1226 - Requires self.suggest_fn (loaded from isicell context) to infer param bounds/types.
1227 - Assumes categorical params in trial_params are stored as category indices (Optuna/LHSIterator path).
1228 - Uses seaborn + matplotlib (imported lazily).
1229 """
1230 import matplotlib.pyplot as plt
1231 import seaborn as sns
1232
1233 # Ensure suggest_fn is available
1234 if self.suggest_fn is None:
1235 self.load_context()
1236 if self.suggest_fn is None:
1237 raise RuntimeError("No suggest entrypoint loaded. Cannot infer parameter bounds/types.")
1238
1239 # Normalize filters
1240 study_names = self._normalize_ids(study_name, cast=str, name="study_name")
1241 trial_ids_norm = self._normalize_ids(trial_ids, cast=int, name="trial_ids")
1242
1243 # Load fitness + params
1244 all_fitness = self.fitness_wide() # columns: study_name, trial_id, obj cols...
1245 all_params = self.params() # MultiIndex (study_name, trial_id)
1246
1247 if all_fitness.empty or all_params.empty:
1248 raise RuntimeError("No completed trials found in DB.")
1249
1250 # Objective column existence
1251 if objective not in all_fitness.columns:
1252 raise KeyError(f"Objective column {objective!r} not found in fitness_wide(). Available: {list(all_fitness.columns)}")
1253
1254 # Filter fitness rows by threshold
1255 fit = all_fitness.loc[all_fitness[objective] < fitness_threshold, ["study_name", "trial_id", objective]].copy()
1256
1257 if study_names is not None:
1258 fit = fit[fit["study_name"].isin(study_names)]
1259
1260 if trial_ids_norm is not None:
1261 fit = fit[fit["trial_id"].isin(trial_ids_norm)]
1262
1263 if fit.empty:
1264 raise RuntimeError("No trial matches the selected filters / fitness threshold.")
1265
1266 # Selected trials index
1267 selected_pairs = set(map(tuple, fit[["study_name", "trial_id"]].to_numpy()))
1268
1269 # Filter params on same (study_name, trial_id)
1270 idx_study = all_params.index.get_level_values("study_name")
1271 idx_trial = all_params.index.get_level_values("trial_id")
1272 mask = [(sn, tid) in selected_pairs for sn, tid in zip(idx_study, idx_trial)]
1273 tmp_params = all_params.loc[mask].copy()
1274
1275 if tmp_params.empty:
1276 raise RuntimeError("No parameters found for selected trials.")
1277
1278 # Bounds from archived suggest function
1279 bounds = LHSIterator.getBoundaries(self.suggest_fn)
1280
1281 # Helper numeric conversion (kept from your logic)
1282 tmp_num_all = tmp_params.copy()
1283 for c in tmp_num_all.columns:
1284 tmp_num_all[c] = pd.to_numeric(tmp_num_all[c], errors="coerce")
1285
1286 # Split params by type from suggest boundaries
1287 num_params = [k for k, v in bounds.items() if v.get("type") in ("int", "float") and k in tmp_params.columns]
1288 cat_params = [k for k, v in bounds.items() if v.get("type") == "categorical" and k in tmp_params.columns]
1289
1290 # ---------- Numeric normalized values ----------
1291 num_rows = []
1292 for p in num_params:
1293 info = bounds[p]
1294 s = pd.to_numeric(tmp_params[p], errors="coerce")
1295
1296 low = float(info["low"])
1297 high = float(info["high"])
1298
1299 if high == low:
1300 continue # borne fixe -> rien à tracer
1301
1302 if info.get("log", False):
1303 # log-normalization if applicable
1304 s = s.where(s > 0)
1305 vals = (np.log(s) - np.log(low)) / (np.log(high) - np.log(low))
1306 else:
1307 vals = (s - low) / (high - low)
1308
1309 vals = vals.clip(0, 1)
1310
1311 label = f"{p} [{low:.3f},{high:.3f}]"
1312 for v in vals.dropna().to_numpy():
1313 num_rows.append({"param": label, "value": float(v), "kind": "numeric"})
1314
1315 num_df = pd.DataFrame(num_rows)
1316
1317 # ---------- Categorical frequencies ----------
1318 cat_rows = []
1319 cat_segment_labels = {} # param_label -> list[(x_center, label, freq)]
1320
1321 for p in cat_params:
1322 info = bounds[p]
1323 choices = info["choices"]
1324
1325 # string labels for display (stable)
1326 def _choice_repr(x):
1327 try:
1328 return repr(choices[int(x)])
1329 except Exception:
1330 # if already decoded / weird value, keep readable fallback
1331 return repr(x)
1332
1333 s = tmp_params[p].map(_choice_repr)
1334 choice_labels = [repr(c) for c in choices]
1335
1336 vc = s.value_counts(normalize=True)
1337 freqs = [float(vc.get(cl, 0.0)) for cl in choice_labels]
1338
1339 # Build segments on x in [0,1]
1340 x0 = 0.0
1341 param_label = f"{p} ({len(choice_labels)} choices)"
1342 cat_segment_labels[param_label] = []
1343
1344 for cl, f in zip(choice_labels, freqs):
1345 if f <= 0:
1346 continue
1347 cat_rows.append({
1348 "param": param_label,
1349 "x0": x0,
1350 "width": f,
1351 "choice": cl,
1352 "freq": f,
1353 "kind": "categorical",
1354 })
1355 cat_segment_labels[param_label].append((x0 + f / 2, cl, f))
1356 x0 += f
1357
1358 cat_df = pd.DataFrame(cat_rows)
1359
1360 # ---------- Unified parameter order ----------
1361 param_order = []
1362 if not num_df.empty:
1363 param_order += list(dict.fromkeys(num_df["param"].tolist()))
1364 if not cat_df.empty:
1365 param_order += [p for p in dict.fromkeys(cat_df["param"].tolist()) if p not in param_order]
1366
1367 if len(param_order) == 0:
1368 raise RuntimeError("No plottable parameter found (check bounds / selected trials).")
1369
1370 # y positions (top to bottom)
1371 ypos = {p: i for i, p in enumerate(param_order)}
1372
1373 # ---------- Plot ----------
1374 fig_h = max(min_fig_height, row_height * len(param_order) + 1.5)
1375 fig = plt.figure(figsize=(figsize_width, fig_h))
1376 ax = plt.gca()
1377
1378 # Numeric violins (same axis [0,1])
1379 if not num_df.empty:
1380 num_df_plot = num_df.copy()
1381 num_df_plot["y"] = num_df_plot["param"].map(ypos)
1382
1383 sns.violinplot(
1384 data=num_df_plot,
1385 x="value",
1386 y="param",
1387 order=param_order,
1388 orient="h",
1389 inner="quartile",
1390 cut=0,
1391 linewidth=1,
1392 ax=ax,
1393 )
1394
1395 # Categorical stacked bars on same axes
1396 if not cat_df.empty:
1397 for _, r in cat_df.iterrows():
1398 y = ypos[r["param"]]
1399 ax.barh(
1400 y=y,
1401 width=r["width"],
1402 left=r["x0"],
1403 height=0.65,
1404 alpha=0.9,
1405 )
1406
1407 # Optional text labels inside segments (only if enough width)
1408 for p, segs in cat_segment_labels.items():
1409 y = ypos[p]
1410 for xc, cl, f in segs:
1411 if f >= min_cat_label_width:
1412 txt = cl
1413 if len(txt) > max_cat_label_len:
1414 txt = txt[: max_cat_label_len - 3] + "..."
1415 ax.text(xc, y, txt, ha="center", va="center", fontsize=label_fontsize)
1416
1417 # Unified axis styling
1418 ax.set_xlim(0, 1)
1419 ax.set_xlabel("Normalized value / category frequency")
1420 ax.set_ylabel("")
1421
1422 # Light separators to improve readability
1423 for i in range(len(param_order)):
1424 ax.axhline(i + 0.5, linewidth=0.3, alpha=0.3)
1425
1426 plt.tight_layout()
1427 plt.show()
dict[str, Any] _encode_context_value(self, Any value)
str _extract_sqlite_path_from_study(self, study)
def trial_add_timeseries(self, trial, str key, Any values, list[str] axes, Optional[list[str]] columns=None, str value_col="value", str layout="aligned")
def set_suggest_entrypoint(self, str name)
def trial_add_data(trial, str key, Any value)
def _set_entrypoint(self, str key, str name)
def check_minimal(self, bool require_run_function=True)
def context_add_many(self, dict[str, Any] mapping)
def study_add_data(self, str key, Any value)
def study_add_axis(self, str name, Any values)
def context_add(self, str name, Any value)
dict[str, Any] study_attrs(self, str study_name)
pd.DataFrame fitness_wide(self, Optional[str] study_name=None)
pd.DataFrame get_trial_timeseries(self, str key, study_name=None, trial_ids=None)
pd.DataFrame params(self, Optional[str] study_name=None)
def build_params(self, int trial_id)
def plot_param_distributions(self, float fitness_threshold=1500, int objective=0, study_name=None, trial_ids=None, float figsize_width=11, float row_height=0.42, float min_fig_height=3.0, int label_fontsize=8, float min_cat_label_width=0.08, int max_cat_label_len=14)
dict[str, str] _read_isicell_entrypoints(self)
dict[str, Any] get_trial_params_flat(self, int trial_id)
def _normalize_ids(self, x, cast=int, name="ids")
str trial_to_study(self, int trial_id)
pd.DataFrame trial_attrs(self, str study_name, Optional[str] key=None)
pd.DataFrame get_trial_data(self, str key, study_name=None, trial_ids=None, raw=False, sep='.')
def __init__(self, str db_path, bool readonly=True)
Any context_value(self, str name)
pd.DataFrame fitness_long(self, Optional[str] study_name=None)
list[tuple[str, str, str]] _read_isicell_context_rows(self)
dict[str, Any] load_context(self)
auto get(const nlohmann::detail::iteration_proxy_value< IteratorType > &i) -> decltype(i.key())
Definition: json.hpp:1787
str variable_to_pickle64b(Any value)
def _capture_external_caller_namespace()
Any pickle64b_to_variable(str payload)
double max(double a, double b)
Computes the maximum of two numbers.
Definition: std.hpp:280