ivoryos 1.3.8__tar.gz → 1.4.0__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 (119) hide show
  1. {ivoryos-1.3.8 → ivoryos-1.4.0}/PKG-INFO +3 -2
  2. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/optimizer/ax_optimizer.py +44 -25
  3. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/optimizer/base_optimizer.py +15 -1
  4. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/optimizer/baybe_optimizer.py +24 -17
  5. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/optimizer/nimo_optimizer.py +26 -24
  6. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/data/data.py +27 -9
  7. ivoryos-1.4.0/ivoryos/routes/data/templates/components/step_card.html +78 -0
  8. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/data/templates/workflow_view.html +14 -5
  9. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/design.py +31 -1
  10. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/design_step.py +2 -1
  11. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/edit_action_form.html +16 -3
  12. ivoryos-1.4.0/ivoryos/routes/design/templates/components/python_code_overlay.html +56 -0
  13. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/experiment_builder.html +1 -0
  14. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/execute/execute.py +36 -7
  15. ivoryos-1.4.0/ivoryos/routes/execute/templates/components/run_tabs.html +60 -0
  16. ivoryos-1.4.0/ivoryos/routes/execute/templates/components/tab_bayesian.html +520 -0
  17. ivoryos-1.4.0/ivoryos/routes/execute/templates/components/tab_configuration.html +383 -0
  18. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
  19. ivoryos-1.4.0/ivoryos/routes/execute/templates/experiment_run.html +30 -0
  20. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/socket_handlers.py +1 -1
  21. ivoryos-1.4.0/ivoryos/static/js/action_handlers.js +384 -0
  22. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/static/js/sortable_design.js +1 -0
  23. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/utils/bo_campaign.py +17 -16
  24. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/utils/db_models.py +122 -18
  25. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/utils/decorators.py +1 -0
  26. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/utils/form.py +22 -5
  27. ivoryos-1.4.0/ivoryos/utils/nest_script.py +314 -0
  28. ivoryos-1.4.0/ivoryos/utils/script_runner.py +789 -0
  29. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/utils/utils.py +11 -1
  30. ivoryos-1.4.0/ivoryos/version.py +1 -0
  31. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos.egg-info/PKG-INFO +3 -2
  32. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos.egg-info/SOURCES.txt +1 -0
  33. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos.egg-info/requires.txt +2 -1
  34. {ivoryos-1.3.8 → ivoryos-1.4.0}/pyproject.toml +2 -1
  35. ivoryos-1.3.8/ivoryos/routes/data/templates/components/step_card.html +0 -42
  36. ivoryos-1.3.8/ivoryos/routes/design/templates/components/python_code_overlay.html +0 -39
  37. ivoryos-1.3.8/ivoryos/routes/execute/templates/components/run_tabs.html +0 -17
  38. ivoryos-1.3.8/ivoryos/routes/execute/templates/components/tab_bayesian.html +0 -398
  39. ivoryos-1.3.8/ivoryos/routes/execute/templates/components/tab_configuration.html +0 -98
  40. ivoryos-1.3.8/ivoryos/routes/execute/templates/experiment_run.html +0 -294
  41. ivoryos-1.3.8/ivoryos/static/js/action_handlers.js +0 -250
  42. ivoryos-1.3.8/ivoryos/utils/script_runner.py +0 -489
  43. ivoryos-1.3.8/ivoryos/version.py +0 -1
  44. {ivoryos-1.3.8 → ivoryos-1.4.0}/LICENSE +0 -0
  45. {ivoryos-1.3.8 → ivoryos-1.4.0}/README.md +0 -0
  46. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/__init__.py +0 -0
  47. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/app.py +0 -0
  48. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/config.py +0 -0
  49. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/optimizer/registry.py +0 -0
  50. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/__init__.py +0 -0
  51. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/api/api.py +0 -0
  52. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/auth/__init__.py +0 -0
  53. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/auth/auth.py +0 -0
  54. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/auth/templates/login.html +0 -0
  55. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/auth/templates/signup.html +0 -0
  56. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/control/__init__.py +0 -0
  57. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/control/control.py +0 -0
  58. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/control/control_file.py +0 -0
  59. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/control/control_new_device.py +0 -0
  60. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/control/templates/controllers.html +0 -0
  61. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/control/templates/controllers_new.html +0 -0
  62. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/control/utils.py +0 -0
  63. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/data/__init__.py +0 -0
  64. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/data/templates/workflow_database.html +0 -0
  65. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/__init__.py +0 -0
  66. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/design_file.py +0 -0
  67. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/action_form.html +0 -0
  68. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/actions_panel.html +0 -0
  69. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/autofill_toggle.html +0 -0
  70. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/canvas.html +0 -0
  71. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/canvas_footer.html +0 -0
  72. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/canvas_header.html +0 -0
  73. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/canvas_main.html +0 -0
  74. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/deck_selector.html +0 -0
  75. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/instruments_panel.html +0 -0
  76. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/modals/drop_modal.html +0 -0
  77. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/modals/json_modal.html +0 -0
  78. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/modals/new_script_modal.html +0 -0
  79. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/modals/rename_modal.html +0 -0
  80. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/modals/saveas_modal.html +0 -0
  81. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/modals.html +0 -0
  82. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/sidebar.html +0 -0
  83. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/design/templates/components/text_to_code_panel.html +0 -0
  84. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/execute/__init__.py +0 -0
  85. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/execute/execute_file.py +0 -0
  86. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/execute/templates/components/error_modal.html +0 -0
  87. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/execute/templates/components/logging_panel.html +0 -0
  88. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/execute/templates/components/progress_panel.html +0 -0
  89. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/execute/templates/components/run_panel.html +0 -0
  90. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/library/__init__.py +0 -0
  91. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/library/library.py +0 -0
  92. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/library/templates/library.html +0 -0
  93. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/main/__init__.py +0 -0
  94. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/main/main.py +0 -0
  95. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/main/templates/help.html +0 -0
  96. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/routes/main/templates/home.html +0 -0
  97. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/server.py +0 -0
  98. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/static/favicon.ico +0 -0
  99. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/static/gui_annotation/Slide1.png +0 -0
  100. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  101. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/static/js/db_delete.js +0 -0
  102. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/static/js/overlay.js +0 -0
  103. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/static/js/script_metadata.js +0 -0
  104. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/static/js/socket_handler.js +0 -0
  105. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/static/js/sortable_card.js +0 -0
  106. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/static/js/ui_state.js +0 -0
  107. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/static/logo.webp +0 -0
  108. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/static/style.css +0 -0
  109. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/templates/base.html +0 -0
  110. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/utils/__init__.py +0 -0
  111. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/utils/client_proxy.py +0 -0
  112. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/utils/global_config.py +0 -0
  113. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/utils/llm_agent.py +0 -0
  114. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/utils/py_to_json.py +0 -0
  115. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/utils/serilize.py +0 -0
  116. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos/utils/task_runner.py +0 -0
  117. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos.egg-info/dependency_links.txt +0 -0
  118. {ivoryos-1.3.8 → ivoryos-1.4.0}/ivoryos.egg-info/top_level.txt +0 -0
  119. {ivoryos-1.3.8 → ivoryos-1.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.3.8
3
+ Version: 1.4.0
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"
@@ -26,7 +27,7 @@ Provides-Extra: optimizer-nimo
26
27
  Requires-Dist: nimo; extra == "optimizer-nimo"
27
28
  Provides-Extra: optimizer
28
29
  Requires-Dist: ax-platform>=1.1.2; extra == "optimizer"
29
- Requires-Dist: baybe; extra == "optimizer"
30
+ Requires-Dist: baybe>=0.14.0; extra == "optimizer"
30
31
  Provides-Extra: doc
31
32
  Requires-Dist: sphinx; extra == "doc"
32
33
  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,22 @@ 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
+ )
113
128
 
114
129
  @staticmethod
115
130
  def get_schema():
@@ -117,29 +132,33 @@ class AxOptimizer(OptimizerBase):
117
132
  "parameter_types": ["range", "choice"],
118
133
  "multiple_objectives": True,
119
134
  # "objective_weights": True,
135
+ "supports_continuous": True,
136
+ "supports_constraints": True,
120
137
  "optimizer_config": {
121
138
  "step_1": {"model": ["Sobol", "Uniform", "Factorial", "Thompson"], "num_samples": 5},
122
139
  "step_2": {"model": ["BoTorch", "SAASBO", "SAAS_MTGP", "Legacy_GPEI", "EB", "EB_Ashr", "ST_MTGP", "BO_MIXED", "Contextual_SACBO"]}
123
140
  },
141
+
124
142
  }
125
143
 
126
- def append_existing_data(self, existing_data):
144
+ def append_existing_data(self, existing_data:DataFrame):
127
145
  """
128
146
  Append existing data to the Ax experiment.
129
147
  :param existing_data: A dictionary containing existing data.
130
148
  """
131
- from pandas import DataFrame
132
- if not existing_data:
133
- return
149
+
134
150
  if isinstance(existing_data, DataFrame):
151
+ if existing_data.empty:
152
+ return
135
153
  existing_data = existing_data.to_dict(orient="records")
136
154
  parameter_names = [i.get("name") for i in self.parameter_space]
137
155
  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}
156
+ for entry in existing_data:
157
+ # for name, value in entry.items():
158
+ # First attach the trial and note the trial index
159
+ parameters = {name: value for name, value in entry.items() if name in parameter_names}
141
160
  trial_index = self.client.attach_trial(parameters=parameters)
142
- raw_data = {name: value for name in existing_data if name in objective_names}
161
+ raw_data = {name: value for name, value in entry.items() if name in objective_names}
143
162
  # Then complete the trial with the existing data
144
163
  self.client.complete_trial(trial_index=trial_index, raw_data=raw_data)
145
164
 
@@ -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,19 @@ class OptimizerBase(ABC):
47
48
  def append_existing_data(self, existing_data):
48
49
  pass
49
50
 
51
+ @staticmethod
52
+ def _create_discrete_search_space(range_with_step=None, value_type ="float"):
53
+ if range_with_step is None:
54
+ range_with_step = []
55
+ import numpy as np
56
+ low, high, step = range_with_step
57
+ values = np.arange(low, high + 1e-9 * step, step).tolist()
58
+ if value_type == "float":
59
+ values = [float(v) for v in values]
60
+ if value_type == "int":
61
+ values = [int(v) for v in values]
62
+ return values
63
+
50
64
  @staticmethod
51
65
  def get_schema():
52
66
  """
@@ -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])
@@ -156,6 +161,8 @@ class BaybeOptimizer(OptimizerBase):
156
161
  return {
157
162
  "parameter_types": ["range", "choice", "substance"],
158
163
  "multiple_objectives": True,
164
+ "supports_continuous": True,
165
+ "supports_constraints": False,
159
166
  "optimizer_config": {
160
167
  "step_1": {"model": ["Random", "FPS"], "num_samples": 10},
161
168
  "step_2": {"model": ["BOTorch", "Naive Hybrid Space"]}
@@ -7,7 +7,8 @@ from ivoryos.optimizer.base_optimizer import OptimizerBase
7
7
 
8
8
 
9
9
  class NIMOOptimizer(OptimizerBase):
10
- def __init__(self, experiment_name:str, parameter_space: list, objective_config: list, optimizer_config: dict, datapath:str):
10
+ def __init__(self, experiment_name:str, parameter_space: list, objective_config: list, optimizer_config: dict,
11
+ parameter_constraints:list=None, datapath:str=None):
11
12
  """
12
13
  :param experiment_name: arbitrary name
13
14
  :param parameter_space: list of parameter names
@@ -17,14 +18,14 @@ class NIMOOptimizer(OptimizerBase):
17
18
  {"name": "param_3", "type": "range", "bounds": [0 10], "value_type": "int"},
18
19
  ]
19
20
  :param objective_config: objective configuration
20
- [
21
+ [
21
22
  {"name": "obj_1", "minimize": True, "weight": 1},
22
23
  {"name": "obj_2", "minimize": False, "weight": 1}
23
24
  ]
24
25
  :param optimizer_config: optimizer configuration
25
- optimizer_config={
26
- "step_1": {"model": "Random", "num_samples": 10},
27
- "step_2": {"model": "BOTorch"}
26
+ {
27
+ "step_1": {"model": "RE", "num_samples": 10},
28
+ "step_2": {"model": "PDC"}
28
29
  }
29
30
  """
30
31
  self.current_step = 0
@@ -33,15 +34,17 @@ class NIMOOptimizer(OptimizerBase):
33
34
  self.objective_config = objective_config
34
35
  self.optimizer_config = optimizer_config
35
36
 
36
- super().__init__(experiment_name, parameter_space, objective_config, optimizer_config, datapath)
37
+ super().__init__(experiment_name, parameter_space, objective_config, optimizer_config, parameter_constraints, datapath)
38
+
39
+ os.makedirs(os.path.join(self.datapath, "nimo_data"), exist_ok=True)
37
40
 
38
41
  step_1 = optimizer_config.get("step_1", {})
39
42
  step_2 = optimizer_config.get("step_2", {})
40
43
  self.step_1_generator = step_1.get("model", "RE")
41
44
  self.step_1_batch_num = step_1.get("num_samples", 1)
42
45
  self.step_2_generator = step_2.get("model", "PDC")
43
- self.candidates = os.path.join(self.datapath, f"{self.experiment_name}_candidates.csv")
44
- self.proposals = os.path.join(self.datapath, f"{self.experiment_name}_proposals.csv")
46
+ self.candidates = os.path.join(self.datapath, "nimo_data", f"{self.experiment_name}_candidates.csv")
47
+ self.proposals = os.path.join(self.datapath, "nimo_data", f"{self.experiment_name}_proposals.csv")
45
48
  self.n_objectives = len(self.objective_config)
46
49
  self._create_candidates_csv()
47
50
 
@@ -50,6 +53,7 @@ class NIMOOptimizer(OptimizerBase):
50
53
  # Extract parameter names and their possible values
51
54
  import pandas as pd
52
55
  import nimo
56
+ import numpy as np
53
57
  if os.path.exists(self.candidates) and nimo.history(self.candidates, self.n_objectives):
54
58
  return
55
59
  param_names = [p["name"] for p in self.parameter_space]
@@ -58,11 +62,9 @@ class NIMOOptimizer(OptimizerBase):
58
62
  for p in self.parameter_space:
59
63
  if p["type"] == "choice" and isinstance(p["bounds"], list):
60
64
  param_values.append(p["bounds"])
61
- elif p["type"] == "range" and len(p["bounds"]) == 2:
62
- low, high = p["bounds"]
63
- num_points = 10 # you can customize this granularity
64
- step = (high - low) / (num_points - 1)
65
- param_values.append([round(low + i * step, 4) for i in range(num_points)])
65
+ elif p["type"] == "range" and len(p["bounds"]) == 3:
66
+ values = self._create_discrete_search_space(range_with_step=p["bounds"],value_type=p["value_type"])
67
+ param_values.append(values)
66
68
  else:
67
69
  raise ValueError(f"Unsupported parameter format: {p}")
68
70
 
@@ -71,7 +73,6 @@ class NIMOOptimizer(OptimizerBase):
71
73
 
72
74
  # Create a DataFrame with parameter columns
73
75
  df = pd.DataFrame(combos, columns=param_names)
74
-
75
76
  # Add empty objective columns
76
77
  for obj in self.objective_config:
77
78
  df[obj["name"]] = ""
@@ -99,20 +100,19 @@ class NIMOOptimizer(OptimizerBase):
99
100
  for _, row in proposals_df.iterrows():
100
101
  proposal = {name: row[name] for name in param_names}
101
102
  proposals.append(proposal)
102
- return proposals[0] if n == 1 else proposals
103
+ return proposals
103
104
 
104
105
  def _convert_observation_to_list(self, obs: dict) -> list:
105
106
  obj_names = [o["name"] for o in self.objective_config]
106
107
  return [obs.get(name, None) for name in obj_names]
107
108
 
108
- def observe(self, results: dict):
109
+ def observe(self, results: list):
109
110
  """
110
111
  observe single output, nimo obj input is [1,2,3] or [[1, 2], [1, 2], [1, 2]] for MO
111
- :param results: {"objective_name": "value"}
112
+ :param results: [{"objective_name": "value"}, {"objective_name": "value"}]]
112
113
  """
113
114
  import nimo
114
- nimo_objective_values = [self._convert_observation_to_list(results)]
115
-
115
+ nimo_objective_values = [self._convert_observation_to_list(result) for result in results]
116
116
  nimo.output_update(input_file=self.proposals,
117
117
  output_file=self.candidates,
118
118
  num_objectives=self.n_objectives,
@@ -126,11 +126,13 @@ class NIMOOptimizer(OptimizerBase):
126
126
  @staticmethod
127
127
  def get_schema():
128
128
  return {
129
- "parameter_types": ["choice"],
129
+ "parameter_types": ["choice", "range"],
130
130
  "multiple_objectives": True,
131
+ "supports_continuous": False,
132
+ "supports_constraints": False,
131
133
  "optimizer_config": {
132
- "step_1": {"model": ["RE", "ES", "PDC"], "num_samples": 5},
133
- "step_2": {"model": ["PHYSBO", "BLOX", "PTR", "SLESA", "BOMP", "COMBI"]}
134
+ "step_1": {"model": ["RE", "ES"], "num_samples": 5},
135
+ "step_2": {"model": ["PHYSBO", "PDC", "BLOX", "PTR", "SLESA", "BOMP", "COMBI"]}
134
136
  },
135
137
  }
136
138
 
@@ -140,7 +142,7 @@ class NIMOOptimizer(OptimizerBase):
140
142
  if __name__ == "__main__":
141
143
  parameter_space = [
142
144
  {"name": "silica", "type": "choice", "bounds": [100], "value_type": "float"},
143
- {"name": "water", "type": "choice", "bounds": [900, 800, 750, 700, 650, 600, 550, 500], "value_type": "float"},
145
+ {"name": "water", "type": "range", "bounds": [500, 900, 50], "value_type": "float"},
144
146
  {"name": "PVA", "type": "choice", "bounds": [0, 0.005, 0.0075, 0.01, 0.05, 0.075, 0.1], "value_type": "float"},
145
147
  {"name": "SDS", "type": "choice", "bounds": [0], "value_type": "float"},
146
148
  {"name": "DTAB", "type": "choice", "bounds": [0, 0.005, 0.0075, 0.01, 0.05, 0.075, 0.1], "value_type": "float"},
@@ -158,5 +160,5 @@ if __name__ == "__main__":
158
160
  nimo_optimizer = NIMOOptimizer(experiment_name="example_experiment", optimizer_config=optimizer_config, parameter_space=parameter_space, objective_config=objective_config)
159
161
  nimo_optimizer.suggest(n=1)
160
162
  nimo_optimizer.observe(
161
- results={"objective": 1.0}
163
+ results=[{"objective": 1.0}]
162
164
  )
@@ -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,