ivoryos 1.3.9__tar.gz → 1.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ivoryos might be problematic. Click here for more details.

Files changed (120) hide show
  1. {ivoryos-1.3.9 → ivoryos-1.4.1}/PKG-INFO +6 -4
  2. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/optimizer/ax_optimizer.py +47 -25
  3. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/optimizer/base_optimizer.py +19 -1
  4. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/optimizer/baybe_optimizer.py +27 -17
  5. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/optimizer/nimo_optimizer.py +25 -16
  6. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/data/data.py +27 -9
  7. ivoryos-1.4.1/ivoryos/routes/data/templates/components/step_card.html +78 -0
  8. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/data/templates/workflow_view.html +14 -5
  9. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/design.py +31 -1
  10. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/design_step.py +2 -1
  11. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/edit_action_form.html +16 -3
  12. ivoryos-1.4.1/ivoryos/routes/design/templates/components/python_code_overlay.html +56 -0
  13. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/experiment_builder.html +1 -0
  14. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/execute/execute.py +71 -13
  15. ivoryos-1.4.1/ivoryos/routes/execute/templates/components/logging_panel.html +56 -0
  16. ivoryos-1.4.1/ivoryos/routes/execute/templates/components/run_tabs.html +60 -0
  17. ivoryos-1.4.1/ivoryos/routes/execute/templates/components/tab_bayesian.html +520 -0
  18. ivoryos-1.4.1/ivoryos/routes/execute/templates/components/tab_configuration.html +383 -0
  19. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
  20. ivoryos-1.4.1/ivoryos/routes/execute/templates/experiment_run.html +30 -0
  21. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/socket_handlers.py +1 -1
  22. ivoryos-1.4.1/ivoryos/static/js/action_handlers.js +384 -0
  23. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/static/js/sortable_design.js +1 -0
  24. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/utils/bo_campaign.py +17 -16
  25. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/utils/db_models.py +122 -18
  26. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/utils/decorators.py +1 -0
  27. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/utils/form.py +32 -12
  28. ivoryos-1.4.1/ivoryos/utils/nest_script.py +314 -0
  29. ivoryos-1.4.1/ivoryos/utils/script_runner.py +790 -0
  30. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/utils/utils.py +11 -1
  31. ivoryos-1.4.1/ivoryos/version.py +1 -0
  32. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos.egg-info/PKG-INFO +6 -4
  33. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos.egg-info/SOURCES.txt +1 -0
  34. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos.egg-info/requires.txt +6 -4
  35. {ivoryos-1.3.9 → ivoryos-1.4.1}/pyproject.toml +4 -2
  36. ivoryos-1.3.9/ivoryos/routes/data/templates/components/step_card.html +0 -42
  37. ivoryos-1.3.9/ivoryos/routes/design/templates/components/python_code_overlay.html +0 -39
  38. ivoryos-1.3.9/ivoryos/routes/execute/templates/components/logging_panel.html +0 -31
  39. ivoryos-1.3.9/ivoryos/routes/execute/templates/components/run_tabs.html +0 -17
  40. ivoryos-1.3.9/ivoryos/routes/execute/templates/components/tab_bayesian.html +0 -398
  41. ivoryos-1.3.9/ivoryos/routes/execute/templates/components/tab_configuration.html +0 -98
  42. ivoryos-1.3.9/ivoryos/routes/execute/templates/experiment_run.html +0 -294
  43. ivoryos-1.3.9/ivoryos/static/js/action_handlers.js +0 -250
  44. ivoryos-1.3.9/ivoryos/utils/script_runner.py +0 -497
  45. ivoryos-1.3.9/ivoryos/version.py +0 -1
  46. {ivoryos-1.3.9 → ivoryos-1.4.1}/LICENSE +0 -0
  47. {ivoryos-1.3.9 → ivoryos-1.4.1}/README.md +0 -0
  48. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/__init__.py +0 -0
  49. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/app.py +0 -0
  50. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/config.py +0 -0
  51. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/optimizer/registry.py +0 -0
  52. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/__init__.py +0 -0
  53. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/api/api.py +0 -0
  54. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/auth/__init__.py +0 -0
  55. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/auth/auth.py +0 -0
  56. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/auth/templates/login.html +0 -0
  57. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/auth/templates/signup.html +0 -0
  58. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/control/__init__.py +0 -0
  59. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/control/control.py +0 -0
  60. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/control/control_file.py +0 -0
  61. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/control/control_new_device.py +0 -0
  62. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/control/templates/controllers.html +0 -0
  63. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/control/templates/controllers_new.html +0 -0
  64. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/control/utils.py +0 -0
  65. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/data/__init__.py +0 -0
  66. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/data/templates/workflow_database.html +0 -0
  67. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/__init__.py +0 -0
  68. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/design_file.py +0 -0
  69. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/action_form.html +0 -0
  70. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/actions_panel.html +0 -0
  71. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/autofill_toggle.html +0 -0
  72. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/canvas.html +0 -0
  73. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/canvas_footer.html +0 -0
  74. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/canvas_header.html +0 -0
  75. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/canvas_main.html +0 -0
  76. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/deck_selector.html +0 -0
  77. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/instruments_panel.html +0 -0
  78. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/modals/drop_modal.html +0 -0
  79. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/modals/json_modal.html +0 -0
  80. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/modals/new_script_modal.html +0 -0
  81. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/modals/rename_modal.html +0 -0
  82. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/modals/saveas_modal.html +0 -0
  83. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/modals.html +0 -0
  84. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/sidebar.html +0 -0
  85. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/design/templates/components/text_to_code_panel.html +0 -0
  86. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/execute/__init__.py +0 -0
  87. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/execute/execute_file.py +0 -0
  88. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/execute/templates/components/error_modal.html +0 -0
  89. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/execute/templates/components/progress_panel.html +0 -0
  90. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/execute/templates/components/run_panel.html +0 -0
  91. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/library/__init__.py +0 -0
  92. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/library/library.py +0 -0
  93. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/library/templates/library.html +0 -0
  94. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/main/__init__.py +0 -0
  95. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/main/main.py +0 -0
  96. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/main/templates/help.html +0 -0
  97. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/routes/main/templates/home.html +0 -0
  98. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/server.py +0 -0
  99. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/static/favicon.ico +0 -0
  100. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/static/gui_annotation/Slide1.png +0 -0
  101. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  102. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/static/js/db_delete.js +0 -0
  103. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/static/js/overlay.js +0 -0
  104. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/static/js/script_metadata.js +0 -0
  105. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/static/js/socket_handler.js +0 -0
  106. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/static/js/sortable_card.js +0 -0
  107. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/static/js/ui_state.js +0 -0
  108. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/static/logo.webp +0 -0
  109. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/static/style.css +0 -0
  110. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/templates/base.html +0 -0
  111. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/utils/__init__.py +0 -0
  112. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/utils/client_proxy.py +0 -0
  113. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/utils/global_config.py +0 -0
  114. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/utils/llm_agent.py +0 -0
  115. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/utils/py_to_json.py +0 -0
  116. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/utils/serilize.py +0 -0
  117. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos/utils/task_runner.py +0 -0
  118. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos.egg-info/dependency_links.txt +0 -0
  119. {ivoryos-1.3.9 → ivoryos-1.4.1}/ivoryos.egg-info/top_level.txt +0 -0
  120. {ivoryos-1.3.9 → ivoryos-1.4.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.3.9
3
+ Version: 1.4.1
4
4
  Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
5
  Author-email: Ivory Zhang <ivoryzhang@chem.ubc.ca>
6
6
  License: MIT
@@ -17,6 +17,7 @@ Requires-Dist: Flask-SQLAlchemy
17
17
  Requires-Dist: Flask-WTF
18
18
  Requires-Dist: SQLAlchemy-Utils
19
19
  Requires-Dist: python-dotenv
20
+ Requires-Dist: pandas
20
21
  Requires-Dist: astor; python_version < "3.9"
21
22
  Provides-Extra: optimizer-ax
22
23
  Requires-Dist: ax-platform; extra == "optimizer-ax"
@@ -24,9 +25,10 @@ Provides-Extra: optimizer-baybe
24
25
  Requires-Dist: baybe; extra == "optimizer-baybe"
25
26
  Provides-Extra: optimizer-nimo
26
27
  Requires-Dist: nimo; extra == "optimizer-nimo"
27
- Provides-Extra: optimizer
28
- Requires-Dist: ax-platform>=1.1.2; extra == "optimizer"
29
- Requires-Dist: baybe; extra == "optimizer"
28
+ Provides-Extra: optimizers
29
+ Requires-Dist: ax-platform>=1.1.2; extra == "optimizers"
30
+ Requires-Dist: baybe>=0.14.0; extra == "optimizers"
31
+ Requires-Dist: nimo; extra == "optimizers"
30
32
  Provides-Extra: doc
31
33
  Requires-Dist: sphinx; extra == "doc"
32
34
  Requires-Dist: sphinx-rtd-theme; extra == "doc"
@@ -1,25 +1,28 @@
1
1
  # optimizers/ax_optimizer.py
2
2
  from typing import Dict
3
3
 
4
-
4
+ from pandas import DataFrame
5
5
 
6
6
  from ivoryos.optimizer.base_optimizer import OptimizerBase
7
7
  from ivoryos.utils.utils import install_and_import
8
8
 
9
9
  class AxOptimizer(OptimizerBase):
10
- def __init__(self, experiment_name, parameter_space, objective_config, optimizer_config=None):
10
+ def __init__(self, experiment_name, parameter_space, objective_config, optimizer_config=None,
11
+ parameter_constraints:list=None, datapath=None):
12
+ self.trial_index_list = None
11
13
  try:
12
14
  from ax.api.client import Client
13
15
  except ImportError as e:
14
16
  install_and_import("ax", "ax-platform")
15
17
  raise ImportError("Please install Ax with pip install ax-platform to use AxOptimizer. Attempting to install Ax...")
16
- super().__init__(experiment_name, parameter_space, objective_config, optimizer_config)
18
+ super().__init__(experiment_name, parameter_space, objective_config, optimizer_config, parameter_constraints, )
17
19
 
18
20
  self.client = Client()
19
21
  # 2. Configure where Ax will search.
20
22
  self.client.configure_experiment(
21
23
  name=experiment_name,
22
- parameters=self._convert_parameter_to_ax_format(parameter_space)
24
+ parameters=self._convert_parameter_to_ax_format(parameter_space),
25
+ parameter_constraints=parameter_constraints
23
26
  )
24
27
  # 3. Configure the objective function.
25
28
  self.client.configure_optimization(objective=self._convert_objective_to_ax_format(objective_config))
@@ -48,12 +51,17 @@ class AxOptimizer(OptimizerBase):
48
51
  ax_params = []
49
52
  for p in parameter_space:
50
53
  if p["type"] == "range":
51
- ax_params.append(
52
- RangeParameterConfig(
53
- name=p["name"],
54
- bounds=tuple(p["bounds"]),
55
- parameter_type=p["value_type"]
56
- ))
54
+ # if step is used here, convert to ChoiceParameterConfig
55
+ if len(p["bounds"]) == 3:
56
+ values = self._create_discrete_search_space(range_with_step=p["bounds"],value_type=p["value_type"])
57
+ ax_params.append(ChoiceParameterConfig(name=p["name"], values=values, parameter_type="int", is_ordered=True))
58
+ else:
59
+ ax_params.append(
60
+ RangeParameterConfig(
61
+ name=p["name"],
62
+ bounds=tuple(p["bounds"]),
63
+ parameter_type=p["value_type"]
64
+ ))
57
65
  elif p["type"] == "choice":
58
66
  ax_params.append(
59
67
  ChoiceParameterConfig(
@@ -101,15 +109,25 @@ class AxOptimizer(OptimizerBase):
101
109
  return GenerationStrategy(steps=[generator_1, generator_2])
102
110
 
103
111
  def suggest(self, n=1):
104
- trial_index, params = self.client.get_next_trials(1).popitem()
105
- self.trial_index = trial_index
106
- return params
112
+ trials = self.client.get_next_trials(n)
113
+ trial_index_list = []
114
+ param_list = []
115
+ for trial_index, params in trials.items():
116
+ trial_index_list.append(trial_index)
117
+ param_list.append(params)
118
+ self.trial_index_list = trial_index_list
119
+ return param_list
107
120
 
108
121
  def observe(self, results):
109
- self.client.complete_trial(
110
- trial_index=self.trial_index,
111
- raw_data=results
112
- )
122
+ for trial_index, result in zip(self.trial_index_list, results):
123
+ obj_only_result = {k: v for k, v in result.items() if k in [obj["name"] for obj in self.objective_config]}
124
+ self.client.complete_trial(
125
+ trial_index=trial_index,
126
+ raw_data=obj_only_result
127
+ )
128
+
129
+ def get_plots(self, plot_type):
130
+ return None
113
131
 
114
132
  @staticmethod
115
133
  def get_schema():
@@ -117,29 +135,33 @@ class AxOptimizer(OptimizerBase):
117
135
  "parameter_types": ["range", "choice"],
118
136
  "multiple_objectives": True,
119
137
  # "objective_weights": True,
138
+ "supports_continuous": True,
139
+ "supports_constraints": True,
120
140
  "optimizer_config": {
121
141
  "step_1": {"model": ["Sobol", "Uniform", "Factorial", "Thompson"], "num_samples": 5},
122
142
  "step_2": {"model": ["BoTorch", "SAASBO", "SAAS_MTGP", "Legacy_GPEI", "EB", "EB_Ashr", "ST_MTGP", "BO_MIXED", "Contextual_SACBO"]}
123
143
  },
144
+
124
145
  }
125
146
 
126
- def append_existing_data(self, existing_data):
147
+ def append_existing_data(self, existing_data:DataFrame):
127
148
  """
128
149
  Append existing data to the Ax experiment.
129
150
  :param existing_data: A dictionary containing existing data.
130
151
  """
131
- from pandas import DataFrame
132
- if not existing_data:
133
- return
152
+
134
153
  if isinstance(existing_data, DataFrame):
154
+ if existing_data.empty:
155
+ return
135
156
  existing_data = existing_data.to_dict(orient="records")
136
157
  parameter_names = [i.get("name") for i in self.parameter_space]
137
158
  objective_names = [i.get("name") for i in self.objective_config]
138
- for name, value in existing_data.items():
139
- # First attach the trial and note the trial index
140
- parameters = {name: value for name in existing_data if name in parameter_names}
159
+ for entry in existing_data:
160
+ # for name, value in entry.items():
161
+ # First attach the trial and note the trial index
162
+ parameters = {name: value for name, value in entry.items() if name in parameter_names}
141
163
  trial_index = self.client.attach_trial(parameters=parameters)
142
- raw_data = {name: value for name in existing_data if name in objective_names}
164
+ raw_data = {name: value for name, value in entry.items() if name in objective_names}
143
165
  # Then complete the trial with the existing data
144
166
  self.client.complete_trial(trial_index=trial_index, raw_data=raw_data)
145
167
 
@@ -5,7 +5,8 @@ from abc import ABC, abstractmethod
5
5
 
6
6
 
7
7
  class OptimizerBase(ABC):
8
- def __init__(self, experiment_name:str, parameter_space: list, objective_config: dict, optimizer_config: dict, datapath:str=None):
8
+ def __init__(self, experiment_name:str, parameter_space: list, objective_config: dict, optimizer_config: dict,
9
+ parameter_constraints:list=None, datapath:str=None):
9
10
  """
10
11
  :param experiment_name: arbitrary name
11
12
  :param parameter_space: list of parameter names
@@ -47,6 +48,23 @@ class OptimizerBase(ABC):
47
48
  def append_existing_data(self, existing_data):
48
49
  pass
49
50
 
51
+ @abstractmethod
52
+ def get_plots(self, plot_type):
53
+ pass
54
+
55
+ @staticmethod
56
+ def _create_discrete_search_space(range_with_step=None, value_type ="float"):
57
+ if range_with_step is None:
58
+ range_with_step = []
59
+ import numpy as np
60
+ low, high, step = range_with_step
61
+ values = np.arange(low, high + 1e-9 * step, step).tolist()
62
+ if value_type == "float":
63
+ values = [float(v) for v in values]
64
+ if value_type == "int":
65
+ values = [int(v) for v in values]
66
+ return values
67
+
50
68
  @staticmethod
51
69
  def get_schema():
52
70
  """
@@ -1,19 +1,21 @@
1
1
  ### Directory: ivoryos/optimizers/baybe_optimizer.py
2
2
  from typing import Dict
3
3
 
4
+ from pandas import DataFrame
4
5
 
5
6
  from ivoryos.utils.utils import install_and_import
6
7
  from ivoryos.optimizer.base_optimizer import OptimizerBase
7
8
 
8
9
  class BaybeOptimizer(OptimizerBase):
9
- def __init__(self, experiment_name, parameter_space, objective_config, optimizer_config):
10
+ def __init__(self, experiment_name, parameter_space, objective_config, optimizer_config,
11
+ parameter_constraints:list=None, datapath=None):
10
12
  try:
11
13
  from baybe import Campaign
12
14
  except ImportError:
13
15
  install_and_import("baybe")
14
16
  print("Please install Baybe with pip install baybe to before register BaybeOptimizer.")
15
17
 
16
- super().__init__(experiment_name, parameter_space, objective_config, optimizer_config)
18
+ super().__init__(experiment_name, parameter_space, objective_config, optimizer_config, parameter_constraints, )
17
19
  self._trial_id = 0
18
20
  self._trials = {}
19
21
 
@@ -25,8 +27,8 @@ class BaybeOptimizer(OptimizerBase):
25
27
 
26
28
 
27
29
  def suggest(self, n=1):
28
- self.df = self.experiment.recommend(batch_size=n)
29
- return self.df.to_dict(orient="records")[0]
30
+ # self.df = self.experiment.recommend(batch_size=n)
31
+ return self.experiment.recommend(batch_size=n).to_dict(orient="records")
30
32
 
31
33
  def observe(self, results, index=None):
32
34
  """
@@ -35,21 +37,19 @@ class BaybeOptimizer(OptimizerBase):
35
37
  :param index: The index of the trial in the DataFrame, if applicable.
36
38
 
37
39
  """
38
- for name, value in results.items():
39
- self.df[name] = [value]
40
- self.experiment.add_measurements(self.df)
40
+ df = DataFrame(results)
41
+ self.experiment.add_measurements(df)
41
42
 
42
- def append_existing_data(self, existing_data: Dict):
43
+ def append_existing_data(self, existing_data: DataFrame):
43
44
  """
44
45
  Append existing data to the Ax experiment.
45
46
  :param existing_data: A dictionary containing existing data.
46
47
  """
47
- import pandas as pd
48
- if not existing_data:
48
+ if existing_data.empty:
49
49
  return
50
50
  # parameter_names = [i.get("name") for i in self.parameter_space]
51
51
  # objective_names = [i.get("name") for i in self.objective_config]
52
- self.experiment.add_measurements(pd.DataFrame(existing_data))
52
+ self.experiment.add_measurements(existing_data)
53
53
  # for name, value in existing_data.items():
54
54
  # # First attach the trial and note the trial index
55
55
  # parameters = {name: value for name in existing_data if name in parameter_names}
@@ -65,9 +65,11 @@ class BaybeOptimizer(OptimizerBase):
65
65
  :param parameter_space: The parameter space configuration.
66
66
  [
67
67
  {"name": "param_1", "type": "range", "bounds": [1.0, 2.0], "value_type": "float"},
68
- {"name": "param_2", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"},
69
- {"name": "param_3", "type": "range", "bounds": [0 10], "value_type": "int"},
70
- {"name": "param_4", "type": "substance", "bounds": ["methanol", "water", "toluene"], "value_type": "str"} #TODO
68
+ {"name": "param_2", "type": "range", "bounds": [1.0, 2.0, 0.5], "value_type": "float"},
69
+
70
+ {"name": "param_3", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"},
71
+ {"name": "param_4", "type": "range", "bounds": [0 10], "value_type": "int"},
72
+ {"name": "param_5", "type": "substance", "bounds": ["methanol", "water", "toluene"], "value_type": "str"} #TODO
71
73
  ]
72
74
  :return: A list of Baybe parameters.
73
75
  """
@@ -77,7 +79,10 @@ class BaybeOptimizer(OptimizerBase):
77
79
  parameters = []
78
80
  for p in parameter_space:
79
81
  if p["type"] == "range":
80
- if p["value_type"] == "float":
82
+ if len(p["bounds"]) == 3:
83
+ values = self._create_discrete_search_space(range_with_step=p["bounds"],value_type=p["value_type"])
84
+ parameters.append(NumericalDiscreteParameter(name=p["name"], values=values))
85
+ elif p["value_type"] == "float":
81
86
  parameters.append(NumericalContinuousParameter(name=p["name"], bounds=p["bounds"]))
82
87
  elif p["value_type"] == "int":
83
88
  values = tuple([int(v) for v in range(p["bounds"][0], p["bounds"][1] + 1)])
@@ -106,10 +111,10 @@ class BaybeOptimizer(OptimizerBase):
106
111
  weights = []
107
112
  for obj in objective_config:
108
113
  obj_name = obj.get("name")
109
- minimize = obj.get("minimize", False)
114
+ minimize = obj.get("minimize", True)
110
115
  weight = obj.get("weight", 1)
111
116
  weights.append(weight)
112
- targets.append(NumericalTarget(name=obj_name, mode="MAX" if minimize else "MIN"))
117
+ targets.append(NumericalTarget(name=obj_name, minimize=minimize))
113
118
 
114
119
  if len(targets) == 1:
115
120
  return SingleTargetObjective(target=targets[0])
@@ -148,6 +153,9 @@ class BaybeOptimizer(OptimizerBase):
148
153
  recommender=step_2_recommender
149
154
  )
150
155
 
156
+ def get_plots(self, plot_type):
157
+ return None
158
+
151
159
  @staticmethod
152
160
  def get_schema():
153
161
  """
@@ -156,6 +164,8 @@ class BaybeOptimizer(OptimizerBase):
156
164
  return {
157
165
  "parameter_types": ["range", "choice", "substance"],
158
166
  "multiple_objectives": True,
167
+ "supports_continuous": True,
168
+ "supports_constraints": False,
159
169
  "optimizer_config": {
160
170
  "step_1": {"model": ["Random", "FPS"], "num_samples": 10},
161
171
  "step_2": {"model": ["BOTorch", "Naive Hybrid Space"]}
@@ -1,4 +1,5 @@
1
1
  ### ivoryos/optimizers/nimo_optimizer.py
2
+ import glob
2
3
  import itertools
3
4
  import os
4
5
 
@@ -7,7 +8,8 @@ from ivoryos.optimizer.base_optimizer import OptimizerBase
7
8
 
8
9
 
9
10
  class NIMOOptimizer(OptimizerBase):
10
- def __init__(self, experiment_name:str, parameter_space: list, objective_config: list, optimizer_config: dict, datapath:str):
11
+ def __init__(self, experiment_name:str, parameter_space: list, objective_config: list, optimizer_config: dict,
12
+ parameter_constraints:list=None, datapath:str=None):
11
13
  """
12
14
  :param experiment_name: arbitrary name
13
15
  :param parameter_space: list of parameter names
@@ -33,7 +35,7 @@ class NIMOOptimizer(OptimizerBase):
33
35
  self.objective_config = objective_config
34
36
  self.optimizer_config = optimizer_config
35
37
 
36
- super().__init__(experiment_name, parameter_space, objective_config, optimizer_config, datapath)
38
+ super().__init__(experiment_name, parameter_space, objective_config, optimizer_config, parameter_constraints, datapath)
37
39
 
38
40
  os.makedirs(os.path.join(self.datapath, "nimo_data"), exist_ok=True)
39
41
 
@@ -52,6 +54,7 @@ class NIMOOptimizer(OptimizerBase):
52
54
  # Extract parameter names and their possible values
53
55
  import pandas as pd
54
56
  import nimo
57
+ import numpy as np
55
58
  if os.path.exists(self.candidates) and nimo.history(self.candidates, self.n_objectives):
56
59
  return
57
60
  param_names = [p["name"] for p in self.parameter_space]
@@ -60,11 +63,9 @@ class NIMOOptimizer(OptimizerBase):
60
63
  for p in self.parameter_space:
61
64
  if p["type"] == "choice" and isinstance(p["bounds"], list):
62
65
  param_values.append(p["bounds"])
63
- elif p["type"] == "range" and len(p["bounds"]) == 2:
64
- low, high = p["bounds"]
65
- num_points = 10 # you can customize this granularity
66
- step = (high - low) / (num_points - 1)
67
- param_values.append([round(low + i * step, 4) for i in range(num_points)])
66
+ elif p["type"] == "range" and len(p["bounds"]) == 3:
67
+ values = self._create_discrete_search_space(range_with_step=p["bounds"],value_type=p["value_type"])
68
+ param_values.append(values)
68
69
  else:
69
70
  raise ValueError(f"Unsupported parameter format: {p}")
70
71
 
@@ -73,7 +74,6 @@ class NIMOOptimizer(OptimizerBase):
73
74
 
74
75
  # Create a DataFrame with parameter columns
75
76
  df = pd.DataFrame(combos, columns=param_names)
76
-
77
77
  # Add empty objective columns
78
78
  for obj in self.objective_config:
79
79
  df[obj["name"]] = ""
@@ -101,20 +101,19 @@ class NIMOOptimizer(OptimizerBase):
101
101
  for _, row in proposals_df.iterrows():
102
102
  proposal = {name: row[name] for name in param_names}
103
103
  proposals.append(proposal)
104
- return proposals[0] if n == 1 else proposals
104
+ return proposals
105
105
 
106
106
  def _convert_observation_to_list(self, obs: dict) -> list:
107
107
  obj_names = [o["name"] for o in self.objective_config]
108
108
  return [obs.get(name, None) for name in obj_names]
109
109
 
110
- def observe(self, results: dict):
110
+ def observe(self, results: list):
111
111
  """
112
112
  observe single output, nimo obj input is [1,2,3] or [[1, 2], [1, 2], [1, 2]] for MO
113
- :param results: {"objective_name": "value"}
113
+ :param results: [{"objective_name": "value"}, {"objective_name": "value"}]]
114
114
  """
115
115
  import nimo
116
- nimo_objective_values = [self._convert_observation_to_list(results)]
117
-
116
+ nimo_objective_values = [self._convert_observation_to_list(result) for result in results]
118
117
  nimo.output_update(input_file=self.proposals,
119
118
  output_file=self.candidates,
120
119
  num_objectives=self.n_objectives,
@@ -124,12 +123,22 @@ class NIMOOptimizer(OptimizerBase):
124
123
  # TODO, history is part of the candidate file, we probably won't need this
125
124
  pass
126
125
 
126
+ def get_plots(self, plot_type):
127
+ import nimo
128
+ nimo.visualization.plot_phase_diagram.plot(input_file=self.candidates,
129
+ fig_folder=os.path.join(self.datapath, "nimo_data"))
130
+ files = sorted(glob.glob(os.path.join(os.path.join(self.datapath, "nimo_data"), "phase_diagram_*.png")))
131
+ if not files:
132
+ return None
133
+ return files[-1]
127
134
 
128
135
  @staticmethod
129
136
  def get_schema():
130
137
  return {
131
- "parameter_types": ["choice"],
138
+ "parameter_types": ["choice", "range"],
132
139
  "multiple_objectives": True,
140
+ "supports_continuous": False,
141
+ "supports_constraints": False,
133
142
  "optimizer_config": {
134
143
  "step_1": {"model": ["RE", "ES"], "num_samples": 5},
135
144
  "step_2": {"model": ["PHYSBO", "PDC", "BLOX", "PTR", "SLESA", "BOMP", "COMBI"]}
@@ -142,7 +151,7 @@ class NIMOOptimizer(OptimizerBase):
142
151
  if __name__ == "__main__":
143
152
  parameter_space = [
144
153
  {"name": "silica", "type": "choice", "bounds": [100], "value_type": "float"},
145
- {"name": "water", "type": "choice", "bounds": [900, 800, 750, 700, 650, 600, 550, 500], "value_type": "float"},
154
+ {"name": "water", "type": "range", "bounds": [500, 900, 50], "value_type": "float"},
146
155
  {"name": "PVA", "type": "choice", "bounds": [0, 0.005, 0.0075, 0.01, 0.05, 0.075, 0.1], "value_type": "float"},
147
156
  {"name": "SDS", "type": "choice", "bounds": [0], "value_type": "float"},
148
157
  {"name": "DTAB", "type": "choice", "bounds": [0, 0.005, 0.0075, 0.01, 0.05, 0.075, 0.1], "value_type": "float"},
@@ -160,5 +169,5 @@ if __name__ == "__main__":
160
169
  nimo_optimizer = NIMOOptimizer(experiment_name="example_experiment", optimizer_config=optimizer_config, parameter_space=parameter_space, objective_config=objective_config)
161
170
  nimo_optimizer.suggest(n=1)
162
171
  nimo_optimizer.observe(
163
- results={"objective": 1.0}
172
+ results=[{"objective": 1.0}]
164
173
  )
@@ -118,25 +118,43 @@ def workflow_phase_data(workflow_id: int):
118
118
 
119
119
  :param workflow_id: workflow id
120
120
  """
121
+
121
122
  workflow = db.session.get(WorkflowRun, workflow_id)
122
123
  if not workflow:
123
124
  return jsonify({})
124
125
 
125
126
  phase_data = {}
126
- # Only plot 'main' phases
127
- main_phases = WorkflowPhase.query.filter_by(run_id=workflow_id, name='main').order_by(
128
- WorkflowPhase.repeat_index).all()
127
+ main_phases = WorkflowPhase.query.filter_by(run_id=workflow_id, name='main') \
128
+ .order_by(WorkflowPhase.repeat_index).all()
129
129
 
130
130
  for phase in main_phases:
131
131
  outputs = phase.outputs or {}
132
132
  phase_index = phase.repeat_index
133
- # Convert each key to list of dicts for x (phase_index) and y (value)
134
133
  phase_data[phase_index] = {}
135
- for k, v in outputs.items():
136
- if isinstance(v, (int, float)):
137
- phase_data[phase_index][k] = [{"x": phase_index, "y": v}]
138
- elif isinstance(v, list) and all(isinstance(i, (int, float)) for i in v):
139
- phase_data[phase_index][k] = v.map(lambda val, idx=0: {"x": phase_index, "y": val})
134
+
135
+ # Normalize everything to a list of dicts
136
+ if isinstance(outputs, dict):
137
+ outputs = [outputs]
138
+ elif isinstance(outputs, list):
139
+ # flatten if it’s nested like [[{...}, {...}]]
140
+ outputs = [
141
+ item for sublist in outputs
142
+ for item in (sublist if isinstance(sublist, list) else [sublist])
143
+ ]
144
+
145
+ # convert each output entry to plotting format
146
+ for out in outputs:
147
+ if not isinstance(out, dict):
148
+ continue
149
+ for k, v in out.items():
150
+ if isinstance(v, (int, float)):
151
+ phase_data[phase_index].setdefault(k, []).append(
152
+ {"x": phase_index, "y": v}
153
+ )
154
+ elif isinstance(v, list) and all(isinstance(i, (int, float)) for i in v):
155
+ phase_data[phase_index].setdefault(k, []).extend(
156
+ {"x": phase_index, "y": val} for val in v
157
+ )
140
158
 
141
159
  return jsonify(phase_data)
142
160
 
@@ -0,0 +1,78 @@
1
+ <div class="card mb-2 {{ 'border-danger text-danger bg-light' if phase.run_error else 'border-secondary' }}">
2
+ <div class="card-body p-2">
3
+ <small class="text-muted">
4
+ <i class="fas fa-play-circle me-1"></i> Start: {{ phase.start_time.strftime('%H:%M:%S') if phase.start_time else 'N/A' }}
5
+ <i class="fas fa-stop-circle ms-2 me-1"></i> End: {{ phase.end_time.strftime('%H:%M:%S') if phase.end_time else 'N/A' }}
6
+ </small>
7
+ {% if phase.parameters %}
8
+ <div class="mt-2">
9
+ <strong>Parameters: </strong>
10
+ {% if phase.parameters is mapping %}
11
+ {% if phase.parameters %}
12
+ <div class="mt-2">
13
+ <strong>Parameters: </strong>
14
+ {% for key, value in phase.parameters.items() %}
15
+ <span class="badge bg-secondary me-1">{{ key }}: {{ value }}</span>
16
+ {% endfor %}
17
+ </div>
18
+ {% endif %}
19
+ {% else %}
20
+ {% for batch in phase.parameters %}
21
+ <div class="mt-1">
22
+ <span class="badge bg-info text-dark me-1">Batch {{ loop.index }}</span>
23
+ {% for key, value in batch.items() %}
24
+ <span class="badge bg-secondary me-1">{{ key }}: {{ value }}</span>
25
+ {% endfor %}
26
+ </div>
27
+ {% endfor %}
28
+ {% endif %}
29
+ </div>
30
+ {% endif %}
31
+
32
+ {% if phase.steps %}
33
+ <div class="mt-2">
34
+ <strong>Steps:</strong>
35
+ <ul class="mb-0">
36
+ {% for step in phase.steps %}
37
+ <li class="{{ 'text-danger' if step.run_error else '' }}">
38
+ {{ step.method_name }}
39
+ <small class="text-muted">
40
+ ({{ step.start_time.strftime('%H:%M:%S') if step.start_time else 'N/A' }} –
41
+ {{ step.end_time.strftime('%H:%M:%S') if step.end_time else 'N/A' }})
42
+ </small>
43
+ </li>
44
+ {% endfor %}
45
+ </ul>
46
+ </div>
47
+ {% endif %}
48
+ {% if phase.outputs %}
49
+ <div class="mt-2">
50
+ <strong>Outputs:</strong>
51
+
52
+ {% if phase.outputs is mapping %}
53
+ {% for key, value in phase.outputs.items() %}
54
+ <span class="badge bg-success me-1">{{ key }}: {{ value }}</span>
55
+ {% endfor %}
56
+
57
+ {% elif phase.outputs is sequence %}
58
+ {% for batch in phase.outputs %}
59
+ <div class="mt-1">
60
+ <span class="badge bg-info text-dark me-1">Batch {{ loop.index }}</span>
61
+ {% if batch is mapping %}
62
+ {% for key, value in batch.items() %}
63
+ <span class="badge bg-success me-1">{{ key }}: {{ value }}</span>
64
+ {% endfor %}
65
+ {% elif batch is sequence %}
66
+ {% for kwargs in batch %}
67
+ {% for key, value in kwargs.items() %}
68
+ <span class="badge bg-success me-1">{{ key }}: {{ value }}</span>
69
+ {% endfor %}
70
+ {% endfor %}
71
+ {% endif %}
72
+ </div>
73
+ {% endfor %}
74
+ {% endif %}
75
+ </div>
76
+ {% endif %}
77
+ </div>
78
+ </div>
@@ -276,12 +276,17 @@
276
276
  repeatKeys.forEach(repeat_index => {
277
277
  const arr = data[repeat_index][selectedKey];
278
278
  if (arr && arr.length) {
279
- arr.forEach(d => {
279
+ const batchCount = arr.length;
280
+
281
+ arr.forEach((d, batchIdx) => {
282
+ // Compute fractional x for batch indexing: e.g. 1.1, 1.2, 1.3
283
+ const xVal = parseFloat(repeat_index) + (batchIdx + 1) / (batchCount + 1);
284
+
280
285
  if (typeof d === 'object' && d.x !== undefined && d.y !== undefined) {
281
- x.push(d.x);
286
+ x.push(xVal);
282
287
  y.push(d.y);
283
288
  } else if (typeof d === 'number') {
284
- x.push(parseInt(repeat_index));
289
+ x.push(xVal);
285
290
  y.push(d);
286
291
  }
287
292
  });
@@ -293,12 +298,16 @@
293
298
  y: y,
294
299
  mode: 'markers',
295
300
  name: selectedKey,
301
+ marker: { size: 8 },
302
+ line: { shape: 'linear', width: 1 },
296
303
  };
297
304
 
298
305
  const layout = {
299
306
  xaxis: {
300
- title: 'Iteration Index',
301
- gridcolor: '#e9ecef'
307
+ title: 'Trial (Batch Sub-index)',
308
+ gridcolor: '#e9ecef',
309
+ tickvals: repeatKeys.map(k => parseFloat(k)),
310
+ ticktext: repeatKeys.map(k => `Trial ${k}`),
302
311
  },
303
312
  yaxis: {
304
313
  title: selectedKey,