ivoryos 1.3.5a0__tar.gz → 1.3.7__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.
Files changed (110) hide show
  1. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/PKG-INFO +15 -2
  2. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/__init__.py +6 -2
  3. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/optimizer/ax_optimizer.py +3 -3
  4. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/optimizer/base_optimizer.py +2 -1
  5. ivoryos-1.3.7/ivoryos/optimizer/nimo_optimizer.py +162 -0
  6. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/optimizer/registry.py +3 -1
  7. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/design.py +12 -3
  8. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/design_step.py +38 -13
  9. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/canvas_main.html +6 -1
  10. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/edit_action_form.html +2 -0
  11. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/server.py +5 -1
  12. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/static/js/action_handlers.js +48 -11
  13. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/utils/bo_campaign.py +1 -1
  14. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/utils/db_models.py +14 -5
  15. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/utils/form.py +8 -4
  16. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/utils/script_runner.py +4 -2
  17. ivoryos-1.3.7/ivoryos/version.py +1 -0
  18. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos.egg-info/PKG-INFO +15 -2
  19. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos.egg-info/SOURCES.txt +1 -0
  20. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos.egg-info/requires.txt +18 -0
  21. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/pyproject.toml +14 -1
  22. ivoryos-1.3.5a0/ivoryos/version.py +0 -1
  23. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/LICENSE +0 -0
  24. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/README.md +0 -0
  25. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/app.py +0 -0
  26. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/config.py +0 -0
  27. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/optimizer/baybe_optimizer.py +0 -0
  28. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/__init__.py +0 -0
  29. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/api/api.py +0 -0
  30. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/auth/__init__.py +0 -0
  31. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/auth/auth.py +0 -0
  32. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/auth/templates/login.html +0 -0
  33. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/auth/templates/signup.html +0 -0
  34. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/control/__init__.py +0 -0
  35. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/control/control.py +0 -0
  36. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/control/control_file.py +0 -0
  37. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/control/control_new_device.py +0 -0
  38. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/control/templates/controllers.html +0 -0
  39. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/control/templates/controllers_new.html +0 -0
  40. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/control/utils.py +0 -0
  41. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/data/__init__.py +0 -0
  42. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/data/data.py +0 -0
  43. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/data/templates/components/step_card.html +0 -0
  44. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/data/templates/workflow_database.html +0 -0
  45. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/data/templates/workflow_view.html +0 -0
  46. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/__init__.py +0 -0
  47. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/design_file.py +0 -0
  48. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/action_form.html +0 -0
  49. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/actions_panel.html +0 -0
  50. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/autofill_toggle.html +0 -0
  51. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/canvas.html +0 -0
  52. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/canvas_footer.html +0 -0
  53. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/canvas_header.html +0 -0
  54. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/deck_selector.html +0 -0
  55. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/instruments_panel.html +0 -0
  56. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/modals/drop_modal.html +0 -0
  57. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/modals/json_modal.html +0 -0
  58. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/modals/new_script_modal.html +0 -0
  59. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/modals/rename_modal.html +0 -0
  60. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/modals/saveas_modal.html +0 -0
  61. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/modals.html +0 -0
  62. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/python_code_overlay.html +0 -0
  63. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/sidebar.html +0 -0
  64. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/components/text_to_code_panel.html +0 -0
  65. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/design/templates/experiment_builder.html +0 -0
  66. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/execute/__init__.py +0 -0
  67. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/execute/execute.py +0 -0
  68. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/execute/execute_file.py +0 -0
  69. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/execute/templates/components/error_modal.html +0 -0
  70. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/execute/templates/components/logging_panel.html +0 -0
  71. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/execute/templates/components/progress_panel.html +0 -0
  72. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/execute/templates/components/run_panel.html +0 -0
  73. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/execute/templates/components/run_tabs.html +0 -0
  74. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/execute/templates/components/tab_bayesian.html +0 -0
  75. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/execute/templates/components/tab_configuration.html +0 -0
  76. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/execute/templates/components/tab_repeat.html +0 -0
  77. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/execute/templates/experiment_run.html +0 -0
  78. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/library/__init__.py +0 -0
  79. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/library/library.py +0 -0
  80. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/library/templates/library.html +0 -0
  81. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/main/__init__.py +0 -0
  82. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/main/main.py +0 -0
  83. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/main/templates/help.html +0 -0
  84. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/routes/main/templates/home.html +0 -0
  85. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/socket_handlers.py +0 -0
  86. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/static/favicon.ico +0 -0
  87. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/static/gui_annotation/Slide1.png +0 -0
  88. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  89. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/static/js/db_delete.js +0 -0
  90. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/static/js/overlay.js +0 -0
  91. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/static/js/script_metadata.js +0 -0
  92. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/static/js/socket_handler.js +0 -0
  93. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/static/js/sortable_card.js +0 -0
  94. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/static/js/sortable_design.js +0 -0
  95. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/static/js/ui_state.js +0 -0
  96. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/static/logo.webp +0 -0
  97. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/static/style.css +0 -0
  98. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/templates/base.html +0 -0
  99. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/utils/__init__.py +0 -0
  100. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/utils/client_proxy.py +0 -0
  101. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/utils/decorators.py +0 -0
  102. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/utils/global_config.py +0 -0
  103. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/utils/llm_agent.py +0 -0
  104. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/utils/py_to_json.py +0 -0
  105. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/utils/serilize.py +0 -0
  106. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/utils/task_runner.py +0 -0
  107. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos/utils/utils.py +0 -0
  108. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos.egg-info/dependency_links.txt +0 -0
  109. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/ivoryos.egg-info/top_level.txt +0 -0
  110. {ivoryos-1.3.5a0 → ivoryos-1.3.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.3.5a0
3
+ Version: 1.3.7
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
@@ -9,6 +9,7 @@ Requires-Python: >=3.7
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: bcrypt
12
+ Requires-Dist: Flask[async]
12
13
  Requires-Dist: Flask-Login
13
14
  Requires-Dist: Flask-Session
14
15
  Requires-Dist: Flask-SocketIO
@@ -17,9 +18,21 @@ Requires-Dist: Flask-WTF
17
18
  Requires-Dist: SQLAlchemy-Utils
18
19
  Requires-Dist: python-dotenv
19
20
  Requires-Dist: astor; python_version < "3.9"
21
+ Provides-Extra: optimizer-ax
22
+ Requires-Dist: ax-platform; extra == "optimizer-ax"
23
+ Provides-Extra: optimizer-baybe
24
+ Requires-Dist: baybe; extra == "optimizer-baybe"
25
+ Provides-Extra: optimizer-nimo
26
+ Requires-Dist: nimo; extra == "optimizer-nimo"
20
27
  Provides-Extra: optimizer
21
- Requires-Dist: ax-platform; extra == "optimizer"
28
+ Requires-Dist: ax-platform>=1.1.2; extra == "optimizer"
22
29
  Requires-Dist: baybe; extra == "optimizer"
30
+ Provides-Extra: doc
31
+ Requires-Dist: sphinx; extra == "doc"
32
+ Requires-Dist: sphinx-rtd-theme; extra == "doc"
33
+ Requires-Dist: sphinxcontrib-httpdomain; extra == "doc"
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest; extra == "dev"
23
36
  Dynamic: license-file
24
37
 
25
38
  [![Documentation Status](https://readthedocs.org/projects/ivoryos/badge/?version=latest)](https://ivoryos.readthedocs.io/en/latest/?badge=latest)
@@ -1,8 +1,8 @@
1
- from ivoryos.server import run
1
+ from ivoryos.server import run, global_config
2
2
  from ivoryos.optimizer.registry import OPTIMIZER_REGISTRY
3
3
  from ivoryos.version import __version__ as ivoryos_version
4
4
  from ivoryos.utils.decorators import block, BUILDING_BLOCKS
5
- from ivoryos.app import app
5
+ from ivoryos.app import app, create_app, socketio, db
6
6
 
7
7
  __all__ = [
8
8
  "block",
@@ -11,4 +11,8 @@ __all__ = [
11
11
  "run",
12
12
  "app",
13
13
  "ivoryos_version",
14
+ "create_app",
15
+ "socketio",
16
+ "global_config",
17
+ "db"
14
18
  ]
@@ -30,7 +30,7 @@ class AxOptimizer(OptimizerBase):
30
30
  @staticmethod
31
31
  def _create_generator_mapping():
32
32
  """Create a mapping from string values to Generator enum members."""
33
- from ax.modelbridge import Generators
33
+ from ax.adapter import Generators
34
34
  return {member.value: member for member in Generators}
35
35
 
36
36
  def _convert_parameter_to_ax_format(self, parameter_space):
@@ -96,8 +96,8 @@ class AxOptimizer(OptimizerBase):
96
96
  step_2 = optimizer_config.get("step_2", {})
97
97
  step_1_generator = step_1.get("model", "Sobol")
98
98
  step_2_generator = step_2.get("model", "BOTorch")
99
- generator_1 = GenerationStep(model=generators.get(step_1_generator), num_trials=step_1.get("num_samples", 5))
100
- generator_2 = GenerationStep(model=generators.get(step_2_generator), num_trials=step_2.get("num_samples", -1))
99
+ generator_1 = GenerationStep(generator=generators.get(step_1_generator), num_trials=step_1.get("num_samples", 5))
100
+ generator_2 = GenerationStep(generator=generators.get(step_2_generator), num_trials=step_2.get("num_samples", -1))
101
101
  return GenerationStrategy(steps=[generator_1, generator_2])
102
102
 
103
103
  def suggest(self, n=1):
@@ -5,7 +5,7 @@ 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):
8
+ def __init__(self, experiment_name:str, parameter_space: list, objective_config: dict, optimizer_config: dict, datapath:str=None):
9
9
  """
10
10
  :param experiment_name: arbitrary name
11
11
  :param parameter_space: list of parameter names
@@ -29,6 +29,7 @@ class OptimizerBase(ABC):
29
29
  self.parameter_space = parameter_space
30
30
  self.objective_config = objective_config
31
31
  self.optimizer_config = optimizer_config
32
+ self.datapath = datapath
32
33
 
33
34
  @abstractmethod
34
35
  def suggest(self, n=1):
@@ -0,0 +1,162 @@
1
+ ### ivoryos/optimizers/nimo_optimizer.py
2
+ import itertools
3
+ import os
4
+
5
+
6
+ from ivoryos.optimizer.base_optimizer import OptimizerBase
7
+
8
+
9
+ class NIMOOptimizer(OptimizerBase):
10
+ def __init__(self, experiment_name:str, parameter_space: list, objective_config: list, optimizer_config: dict, datapath:str):
11
+ """
12
+ :param experiment_name: arbitrary name
13
+ :param parameter_space: list of parameter names
14
+ [
15
+ {"name": "param_1", "type": "range", "bounds": [1.0, 2.0], "value_type": "float"},
16
+ {"name": "param_2", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"},
17
+ {"name": "param_3", "type": "range", "bounds": [0 10], "value_type": "int"},
18
+ ]
19
+ :param objective_config: objective configuration
20
+ [
21
+ {"name": "obj_1", "minimize": True, "weight": 1},
22
+ {"name": "obj_2", "minimize": False, "weight": 1}
23
+ ]
24
+ :param optimizer_config: optimizer configuration
25
+ optimizer_config={
26
+ "step_1": {"model": "Random", "num_samples": 10},
27
+ "step_2": {"model": "BOTorch"}
28
+ }
29
+ """
30
+ self.current_step = 0
31
+ self.experiment_name = experiment_name
32
+ self.parameter_space = parameter_space
33
+ self.objective_config = objective_config
34
+ self.optimizer_config = optimizer_config
35
+
36
+ super().__init__(experiment_name, parameter_space, objective_config, optimizer_config, datapath)
37
+
38
+ step_1 = optimizer_config.get("step_1", {})
39
+ step_2 = optimizer_config.get("step_2", {})
40
+ self.step_1_generator = step_1.get("model", "RE")
41
+ self.step_1_batch_num = step_1.get("num_samples", 1)
42
+ 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")
45
+ self.n_objectives = len(self.objective_config)
46
+ self._create_candidates_csv()
47
+
48
+
49
+ def _create_candidates_csv(self):
50
+ # Extract parameter names and their possible values
51
+ import pandas as pd
52
+ import nimo
53
+ if os.path.exists(self.candidates) and nimo.history(self.candidates, self.n_objectives):
54
+ return
55
+ param_names = [p["name"] for p in self.parameter_space]
56
+
57
+ param_values = []
58
+ for p in self.parameter_space:
59
+ if p["type"] == "choice" and isinstance(p["bounds"], list):
60
+ 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)])
66
+ else:
67
+ raise ValueError(f"Unsupported parameter format: {p}")
68
+
69
+ # Generate all possible combinations
70
+ combos = list(itertools.product(*param_values))
71
+
72
+ # Create a DataFrame with parameter columns
73
+ df = pd.DataFrame(combos, columns=param_names)
74
+
75
+ # Add empty objective columns
76
+ for obj in self.objective_config:
77
+ df[obj["name"]] = ""
78
+
79
+ # Save to CSV
80
+ df.to_csv(self.candidates, index=False)
81
+
82
+
83
+ def suggest(self, n=1):
84
+ import pandas as pd
85
+ import nimo
86
+ method = self.step_1_generator if self.current_step <= self.step_1_batch_num else self.step_2_generator
87
+ nimo.selection(method = method,
88
+ input_file = self.candidates,
89
+ output_file = self.proposals,
90
+ num_objectives = self.n_objectives,
91
+ num_proposals = n)
92
+ self.current_step += 1
93
+ # Read proposals from CSV file
94
+ proposals_df = pd.read_csv(self.proposals)
95
+ # Get parameter names
96
+ param_names = [p["name"] for p in self.parameter_space]
97
+ # Convert proposals to list of parameter dictionaries
98
+ proposals = []
99
+ for _, row in proposals_df.iterrows():
100
+ proposal = {name: row[name] for name in param_names}
101
+ proposals.append(proposal)
102
+ return proposals[0] if n == 1 else proposals
103
+
104
+ def _convert_observation_to_list(self, obs: dict) -> list:
105
+ obj_names = [o["name"] for o in self.objective_config]
106
+ return [obs.get(name, None) for name in obj_names]
107
+
108
+ def observe(self, results: dict):
109
+ """
110
+ 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
+ """
113
+ import nimo
114
+ nimo_objective_values = [self._convert_observation_to_list(results)]
115
+
116
+ nimo.output_update(input_file=self.proposals,
117
+ output_file=self.candidates,
118
+ num_objectives=self.n_objectives,
119
+ objective_values=nimo_objective_values)
120
+
121
+ def append_existing_data(self, existing_data):
122
+ # TODO, history is part of the candidate file, we probably won't need this
123
+ pass
124
+
125
+
126
+ @staticmethod
127
+ def get_schema():
128
+ return {
129
+ "parameter_types": ["choice"],
130
+ "multiple_objectives": True,
131
+ "optimizer_config": {
132
+ "step_1": {"model": ["RE", "ES", "PDC"], "num_samples": 5},
133
+ "step_2": {"model": ["PHYSBO", "BLOX", "PTR", "SLESA", "BOMP", "COMBI"]}
134
+ },
135
+ }
136
+
137
+
138
+
139
+
140
+ if __name__ == "__main__":
141
+ parameter_space = [
142
+ {"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"},
144
+ {"name": "PVA", "type": "choice", "bounds": [0, 0.005, 0.0075, 0.01, 0.05, 0.075, 0.1], "value_type": "float"},
145
+ {"name": "SDS", "type": "choice", "bounds": [0], "value_type": "float"},
146
+ {"name": "DTAB", "type": "choice", "bounds": [0, 0.005, 0.0075, 0.01, 0.05, 0.075, 0.1], "value_type": "float"},
147
+ {"name": "PVP", "type": "choice", "bounds": [0], "value_type": "float"},
148
+ ]
149
+ objective_config = [
150
+ {"name": "objective", "minimize": False, "weight": 1},
151
+
152
+ ]
153
+ optimizer_config = {
154
+ "step_1": {"model": "RE", "num_samples": 10},
155
+ "step_2": {"model": "PDC"}
156
+ }
157
+
158
+ nimo_optimizer = NIMOOptimizer(experiment_name="example_experiment", optimizer_config=optimizer_config, parameter_space=parameter_space, objective_config=objective_config)
159
+ nimo_optimizer.suggest(n=1)
160
+ nimo_optimizer.observe(
161
+ results={"objective": 1.0}
162
+ )
@@ -2,8 +2,10 @@
2
2
 
3
3
  from ivoryos.optimizer.ax_optimizer import AxOptimizer
4
4
  from ivoryos.optimizer.baybe_optimizer import BaybeOptimizer
5
+ from ivoryos.optimizer.nimo_optimizer import NIMOOptimizer
5
6
 
6
7
  OPTIMIZER_REGISTRY = {
7
8
  "ax": AxOptimizer,
8
- "baybe": BaybeOptimizer
9
+ "baybe": BaybeOptimizer,
10
+ "nimo": NIMOOptimizer,
9
11
  }
@@ -87,8 +87,11 @@ def experiment_builder():
87
87
 
88
88
  # edit_action_info = session.get("edit_action")
89
89
 
90
-
91
- exec_string = script.python_script if script.python_script else script.compile(current_app.config['SCRIPT_FOLDER'])
90
+ try:
91
+ exec_string = script.python_script if script.python_script else script.compile(current_app.config['SCRIPT_FOLDER'])
92
+ except Exception as e:
93
+ exec_string = {}
94
+ flash(f"Error in Python script: {e}")
92
95
  session['python_code'] = exec_string
93
96
 
94
97
  design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
@@ -382,7 +385,13 @@ def methods_handler(instrument: str = ''):
382
385
  success = False
383
386
  msg = [f"{field}: {', '.join(messages)}" for field, messages in form.errors.items()]
384
387
  utils.post_script_file(script)
385
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
388
+ #TODO
389
+ try:
390
+ exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
391
+ except Exception as e:
392
+ exec_string = {}
393
+ msg = f"Compilation failed: {str(e)}"
394
+ # exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
386
395
  session['python_code'] = exec_string
387
396
  design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
388
397
  html = render_template("components/canvas_main.html", script=script, buttons_dict=design_buttons)
@@ -23,12 +23,16 @@ def get_step(uuid: int):
23
23
  """
24
24
  script = utils.get_script_file()
25
25
  action = script.find_by_uuid(uuid)
26
- if request.method == 'GET':
27
- # forms = create_form_from_action(action, script=script)
26
+ if action is None:
27
+ return jsonify({"warning": "Step not found, please refresh the page."}), 404
28
+
29
+ elif request.method == 'GET':
30
+ forms = create_form_from_action(action, script=script)
28
31
  # session['edit_action'] = action
29
32
  return render_template("components/edit_action_form.html",
30
33
  action=action,
31
- forms=create_form_from_action(action, script=script))
34
+ forms=forms)
35
+
32
36
 
33
37
 
34
38
  @steps.post("/draft/steps/<int:uuid>")
@@ -47,6 +51,7 @@ def save_step(uuid: int):
47
51
  """
48
52
  script = utils.get_script_file()
49
53
  action = script.find_by_uuid(uuid)
54
+ warning = None
50
55
  if action is not None:
51
56
  forms = create_form_from_action(action, script=script)
52
57
  kwargs = {field.name: field.data for field in forms if field.name != 'csrf_token'}
@@ -55,14 +60,19 @@ def save_step(uuid: int):
55
60
  kwargs = script.validate_variables(kwargs)
56
61
  script.update_by_uuid(uuid=uuid, args=kwargs, output=save_as)
57
62
  else:
58
- flash(forms.errors)
63
+ warning = f"Compilation failed: {str(forms.errors)}"
59
64
  utils.post_script_file(script)
60
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
65
+ try:
66
+ exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
67
+ except Exception as e:
68
+ exec_string = {}
69
+ warning = f"Compilation failed: {str(e)}"
61
70
  session['python_code'] = exec_string
62
71
  design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
63
72
  return render_template("components/canvas_main.html",
64
- script=script,
65
- buttons_dict=design_buttons)
73
+ script=script,
74
+ buttons_dict=design_buttons,
75
+ warning=warning)
66
76
 
67
77
  @steps.delete("/draft/steps/<int:uuid>")
68
78
  def delete_step(uuid: int):
@@ -82,12 +92,17 @@ def delete_step(uuid: int):
82
92
  if request.method == 'DELETE':
83
93
  script.delete_action(uuid)
84
94
  utils.post_script_file(script)
85
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
95
+ warning = None
96
+ try:
97
+ exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
98
+ except Exception as e:
99
+ exec_string = {}
100
+ warning = f"Compilation failed: {str(e)}"
86
101
  session['python_code'] = exec_string
87
102
  design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
88
103
  return render_template("components/canvas_main.html",
89
104
  script=script,
90
- buttons_dict=design_buttons)
105
+ buttons_dict=design_buttons, warning=warning)
91
106
 
92
107
 
93
108
  @steps.route("/draft/steps/<int:uuid>/duplicate", methods=["POST"], strict_slashes=False,)
@@ -107,13 +122,18 @@ def duplicate_action(uuid: int):
107
122
  script = utils.get_script_file()
108
123
  script.duplicate_action(uuid)
109
124
  utils.post_script_file(script)
110
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
125
+ warning = None
126
+ try:
127
+ exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
128
+ except Exception as e:
129
+ exec_string = {}
130
+ warning = f"Compilation failed: {str(e)}"
111
131
  session['python_code'] = exec_string
112
132
  design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
113
133
 
114
134
  return render_template("components/canvas_main.html",
115
135
  script=script,
116
- buttons_dict=design_buttons)
136
+ buttons_dict=design_buttons, warning=warning)
117
137
 
118
138
 
119
139
  @steps.route("/draft/steps/order", methods=['POST'])
@@ -133,13 +153,18 @@ def update_list():
133
153
  script = utils.get_script_file()
134
154
  script.currently_editing_order = order.split(",", len(script.currently_editing_script))
135
155
  script.sort_actions()
156
+ warning = None
136
157
 
137
158
  utils.post_script_file(script)
138
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
159
+ try:
160
+ exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
161
+ except Exception as e:
162
+ exec_string = {}
163
+ warning = f"Compilation failed: {str(e)}"
139
164
  session['python_code'] = exec_string
140
165
 
141
166
  # Return the updated canvas HTML instead of JSON
142
167
  design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
143
168
  return render_template("components/canvas_main.html",
144
169
  script=script,
145
- buttons_dict=design_buttons)
170
+ buttons_dict=design_buttons, warning=warning)
@@ -31,4 +31,9 @@
31
31
  </div>
32
32
  <div class="python-code-wrapper" id="python-code-wrapper">
33
33
  {% include 'components/python_code_overlay.html' %}
34
- </div>
34
+ </div>
35
+
36
+
37
+ {% if warning %}
38
+ <div id="warning" style="display:none;">{{ warning }}</div>
39
+ {% endif %}
@@ -6,6 +6,7 @@
6
6
  <i class="bi bi-arrow-return-left"></i>
7
7
  </a>
8
8
  </div>
9
+ {% if action %}
9
10
  <h5> {{ action['action'] | format_name }} </h5>
10
11
  <form role="form" method='POST' name="{{instrument}}"
11
12
  action="{{ url_for('design.design_steps.get_step', uuid=action['uuid']) }}"
@@ -36,3 +37,4 @@
36
37
  <button type="submit" class="btn btn-primary">Save</button>
37
38
  <button type="button" class="btn btn-primary" id="back">Back</button>
38
39
  </form>
40
+ {% endif %}
@@ -50,6 +50,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
50
50
  blueprint_plugins: Union[list, Blueprint] = [],
51
51
  exclude_names: list = [],
52
52
  notification_handler=None,
53
+ optimizer_registry: dict = None,
53
54
  ):
54
55
  """
55
56
  Start ivoryOS app server.
@@ -91,7 +92,10 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
91
92
  app.config["LOGGERS_PATH"] = logger_output_name or app.config["LOGGERS_PATH"] # default.log
92
93
  logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
93
94
  dummy_deck_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["DUMMY_DECK"])
94
- global_config.optimizers = OPTIMIZER_REGISTRY
95
+ if optimizer_registry:
96
+ global_config.optimizers = optimizer_registry
97
+ else:
98
+ global_config.optimizers = OPTIMIZER_REGISTRY
95
99
  if module:
96
100
  app.config["MODULE"] = module
97
101
  app.config["OFF_LINE"] = False
@@ -103,6 +103,12 @@ function submitEditForm(event) {
103
103
  document.getElementById('instrument-panel').innerHTML = previousHtmlState;
104
104
  previousHtmlState = null; // Clear the stored state
105
105
  }
106
+ const parser = new DOMParser();
107
+ const doc = parser.parseFromString(html, 'text/html');
108
+ const warningDiv = doc.querySelector('#warning');
109
+ if (warningDiv && warningDiv.textContent.trim()) {
110
+ alert(warningDiv.textContent.trim()); // or use a nicer toast
111
+ }
106
112
  }
107
113
  })
108
114
  .catch(error => {
@@ -149,6 +155,13 @@ function duplicateAction(uuid) {
149
155
  .then(response => response.text())
150
156
  .then(html => {
151
157
  updateActionCanvas(html);
158
+
159
+ const parser = new DOMParser();
160
+ const doc = parser.parseFromString(html, 'text/html');
161
+ const warningDiv = doc.querySelector('#warning');
162
+ if (warningDiv && warningDiv.textContent.trim()) {
163
+ alert(warningDiv.textContent.trim()); // or use a nicer toast
164
+ }
152
165
  })
153
166
  .catch(error => console.error('Error:', error));
154
167
  }
@@ -159,33 +172,50 @@ function editAction(uuid) {
159
172
  return;
160
173
  }
161
174
 
162
- // Save current state before fetching new content
163
175
  previousHtmlState = document.getElementById('instrument-panel').innerHTML;
164
176
 
165
177
  fetch(scriptStepUrl.replace('0', uuid), {
166
- method: 'GET',
178
+ method: 'GET', // no need for Content-Type on GET
167
179
  headers: {
168
180
  'Content-Type': 'application/json'
169
181
  }
170
182
  })
171
- .then(response => response.text())
183
+ .then(response => {
184
+ if (!response.ok) {
185
+ return response.json().then(err => {
186
+ if (err.warning) {
187
+ alert(err.warning); // <-- should fire now
188
+ }
189
+ // restore panel so user isn't stuck
190
+ if (previousHtmlState) {
191
+ document.getElementById('instrument-panel').innerHTML = previousHtmlState;
192
+ previousHtmlState = null;
193
+ }
194
+ throw new Error("Step fetch failed: " + response.status);
195
+ });
196
+ }
197
+ return response.text();
198
+ })
172
199
  .then(html => {
173
200
  document.getElementById('instrument-panel').innerHTML = html;
174
201
 
175
- // Add click handler for back button
176
- document.getElementById('back').addEventListener('click', function(e) {
177
- e.preventDefault();
178
- if (previousHtmlState) {
179
- document.getElementById('instrument-panel').innerHTML = previousHtmlState;
180
- previousHtmlState = null; // Clear the stored state
181
- }
182
- });
202
+ const backButton = document.getElementById('back');
203
+ if (backButton) {
204
+ backButton.addEventListener('click', function(e) {
205
+ e.preventDefault();
206
+ if (previousHtmlState) {
207
+ document.getElementById('instrument-panel').innerHTML = previousHtmlState;
208
+ previousHtmlState = null;
209
+ }
210
+ });
211
+ }
183
212
  })
184
213
  .catch(error => console.error('Error:', error));
185
214
  }
186
215
 
187
216
 
188
217
 
218
+
189
219
  function deleteAction(uuid) {
190
220
  if (!uuid) {
191
221
  console.error('Invalid UUID');
@@ -202,6 +232,13 @@ function deleteAction(uuid) {
202
232
  .then(html => {
203
233
  // Find the first list element's content and replace it
204
234
  updateActionCanvas(html);
235
+ // Optionally, check if a warning element exists
236
+ const parser = new DOMParser();
237
+ const doc = parser.parseFromString(html, 'text/html');
238
+ const warningDiv = doc.querySelector('#warning');
239
+ if (warningDiv && warningDiv.textContent.trim()) {
240
+ alert(warningDiv.textContent.trim()); // or use a nicer toast
241
+ }
205
242
  })
206
243
  .catch(error => console.error('Error:', error));
207
244
  }
@@ -209,7 +209,7 @@ def parse_optimization_form(form_data: Dict[str, str]):
209
209
  parameter["bounds"] = bounds
210
210
 
211
211
  elif value == "choice":
212
- choices_field = f"{param_name}_choices"
212
+ choices_field = f"{param_name}_value"
213
213
  if choices_field in form_data and form_data[choices_field]:
214
214
  # Split choices by comma and clean whitespace
215
215
  choices = [choice.strip() for choice in form_data[choices_field].split(',')]
@@ -644,12 +644,21 @@ class Script(db.Model):
644
644
  args_str = args_str.replace(f"'#{args[arg][1:]}'", args[arg][1:])
645
645
  elif isinstance(args[arg], dict):
646
646
  # print(args[arg])
647
- variables = self.get_variables()
647
+ if not args[arg]:
648
+ continue
649
+ # Extract the variable name (first key in the dict)
648
650
  value = next(iter(args[arg]))
649
- if value not in variables:
650
- raise ValueError(f"Variable ({value}) is not defined.")
651
- args_str = args_str.replace(f"{args[arg]}", next(iter(args[arg])))
652
- # elif self._is_variable(arg):
651
+ var_type = args[arg].get(value)
652
+
653
+ # Only process if it's a function_output variable reference
654
+ if var_type == "function_output":
655
+ variables = self.get_variables()
656
+ if value not in variables:
657
+ raise ValueError(f"Variable ({value}) is not defined.")
658
+ # Replace the dict string representation with just the variable name
659
+ args_str = args_str.replace(f"{args[arg]}", value)
660
+
661
+ # elif self._is_variable(arg):
653
662
  # print("is variable")
654
663
  # args_str = args_str.replace(f"'{args[arg]}'", args[arg])
655
664
  return args_str
@@ -405,7 +405,7 @@ def create_form_from_action(action: dict, script=None, design=True):
405
405
  for name, param_type in arg_types.items():
406
406
  # formatted_param_name = format_name(name)
407
407
  value = args.get(name, "")
408
- if type(value) is dict:
408
+ if type(value) is dict and value:
409
409
  value = next(iter(value))
410
410
  field_kwargs = {
411
411
  "label": name,
@@ -556,9 +556,13 @@ def _action_button(action: dict, variables: dict):
556
556
  arg_list = []
557
557
  for k, v in action['args'].items():
558
558
  if isinstance(v, dict):
559
- value = next(iter(v)) # Extract the first key if it's a dict
560
- # show warning color for variable calling when there is no definition
561
- style = "background-color: khaki" if value not in variables.keys() else ""
559
+ if not v:
560
+ value = v # Keep the original value if not a dict
561
+ else:
562
+ value = next(iter(v)) # Extract the first key if it's a dict
563
+ # show warning color for variable calling when there is no definition
564
+
565
+ style = "background-color: khaki" if v.get(value) == "function_output" and value not in variables.keys() else ""
562
566
  else:
563
567
  value = v # Keep the original value if not a dict
564
568
  arg_list.append(f"{k} = {value}") # Format the key-value pair
@@ -195,11 +195,12 @@ class ScriptRunner:
195
195
 
196
196
  # Update exec_locals with the returned locals
197
197
  exec_locals.update(result_locals)
198
- exec_locals.pop("__async_exec_wrapper", None)
198
+
199
199
 
200
200
  else:
201
- print("just exec synchronously")
201
+ # print("just exec synchronously")
202
202
  exec(line, exec_globals, exec_locals)
203
+ exec_globals.update(exec_locals)
203
204
  # return locals_dict
204
205
  # exec(line, exec_globals, exec_locals)
205
206
  # step.run_error = False
@@ -217,6 +218,7 @@ class ScriptRunner:
217
218
 
218
219
  step.run_error = True
219
220
  self.toggle_pause()
221
+ exec_locals.pop("__async_exec_wrapper", None)
220
222
  step.end_time = datetime.now()
221
223
  step.output = exec_locals
222
224
  db.session.commit()
@@ -0,0 +1 @@
1
+ __version__ = "1.3.7"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.3.5a0
3
+ Version: 1.3.7
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
@@ -9,6 +9,7 @@ Requires-Python: >=3.7
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: bcrypt
12
+ Requires-Dist: Flask[async]
12
13
  Requires-Dist: Flask-Login
13
14
  Requires-Dist: Flask-Session
14
15
  Requires-Dist: Flask-SocketIO
@@ -17,9 +18,21 @@ Requires-Dist: Flask-WTF
17
18
  Requires-Dist: SQLAlchemy-Utils
18
19
  Requires-Dist: python-dotenv
19
20
  Requires-Dist: astor; python_version < "3.9"
21
+ Provides-Extra: optimizer-ax
22
+ Requires-Dist: ax-platform; extra == "optimizer-ax"
23
+ Provides-Extra: optimizer-baybe
24
+ Requires-Dist: baybe; extra == "optimizer-baybe"
25
+ Provides-Extra: optimizer-nimo
26
+ Requires-Dist: nimo; extra == "optimizer-nimo"
20
27
  Provides-Extra: optimizer
21
- Requires-Dist: ax-platform; extra == "optimizer"
28
+ Requires-Dist: ax-platform>=1.1.2; extra == "optimizer"
22
29
  Requires-Dist: baybe; extra == "optimizer"
30
+ Provides-Extra: doc
31
+ Requires-Dist: sphinx; extra == "doc"
32
+ Requires-Dist: sphinx-rtd-theme; extra == "doc"
33
+ Requires-Dist: sphinxcontrib-httpdomain; extra == "doc"
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest; extra == "dev"
23
36
  Dynamic: license-file
24
37
 
25
38
  [![Documentation Status](https://readthedocs.org/projects/ivoryos/badge/?version=latest)](https://ivoryos.readthedocs.io/en/latest/?badge=latest)
@@ -15,6 +15,7 @@ ivoryos.egg-info/top_level.txt
15
15
  ivoryos/optimizer/ax_optimizer.py
16
16
  ivoryos/optimizer/base_optimizer.py
17
17
  ivoryos/optimizer/baybe_optimizer.py
18
+ ivoryos/optimizer/nimo_optimizer.py
18
19
  ivoryos/optimizer/registry.py
19
20
  ivoryos/routes/__init__.py
20
21
  ivoryos/routes/api/api.py
@@ -1,4 +1,5 @@
1
1
  bcrypt
2
+ Flask[async]
2
3
  Flask-Login
3
4
  Flask-Session
4
5
  Flask-SocketIO
@@ -10,6 +11,23 @@ python-dotenv
10
11
  [:python_version < "3.9"]
11
12
  astor
12
13
 
14
+ [dev]
15
+ pytest
16
+
17
+ [doc]
18
+ sphinx
19
+ sphinx-rtd-theme
20
+ sphinxcontrib-httpdomain
21
+
13
22
  [optimizer]
23
+ ax-platform>=1.1.2
24
+ baybe
25
+
26
+ [optimizer-ax]
14
27
  ax-platform
28
+
29
+ [optimizer-baybe]
15
30
  baybe
31
+
32
+ [optimizer-nimo]
33
+ nimo
@@ -14,6 +14,7 @@ authors = [
14
14
  ]
15
15
  dependencies = [
16
16
  "bcrypt",
17
+ "Flask[async]",
17
18
  "Flask-Login",
18
19
  "Flask-Session",
19
20
  "Flask-SocketIO",
@@ -25,7 +26,19 @@ dependencies = [
25
26
  ]
26
27
 
27
28
  [project.optional-dependencies]
28
- optimizer = ["ax-platform", "baybe"]
29
+ optimizer-ax = ["ax-platform"]
30
+ optimizer-baybe = ["baybe"]
31
+ optimizer-nimo = ["nimo"]
32
+ optimizer = [
33
+ "ax-platform>=1.1.2",
34
+ "baybe"
35
+ ]
36
+ doc = [
37
+ "sphinx",
38
+ "sphinx-rtd-theme",
39
+ "sphinxcontrib-httpdomain"
40
+ ]
41
+ dev = ["pytest"]
29
42
 
30
43
  [project.urls]
31
44
  Homepage = "https://gitlab.com/heingroup/ivoryos"
@@ -1 +0,0 @@
1
- __version__ = "1.3.5a0"
File without changes
File without changes
File without changes
File without changes
File without changes