ivoryos 1.0.9__py3-none-any.whl → 1.2.0__py3-none-any.whl
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.
- ivoryos/__init__.py +26 -7
- ivoryos/routes/api/api.py +56 -0
- ivoryos/routes/auth/auth.py +5 -5
- ivoryos/routes/control/control.py +77 -372
- ivoryos/routes/control/control_file.py +36 -0
- ivoryos/routes/control/control_new_device.py +142 -0
- ivoryos/routes/control/templates/controllers.html +166 -0
- ivoryos/routes/control/templates/controllers_new.html +112 -0
- ivoryos/routes/control/utils.py +38 -0
- ivoryos/routes/data/data.py +129 -0
- ivoryos/routes/data/templates/components/step_card.html +13 -0
- ivoryos/routes/{database/templates/database → data/templates}/workflow_database.html +14 -8
- ivoryos/routes/{database/templates/database → data/templates}/workflow_view.html +3 -3
- ivoryos/routes/design/__init__.py +4 -0
- ivoryos/routes/design/design.py +298 -656
- ivoryos/routes/design/design_file.py +68 -0
- ivoryos/routes/design/design_step.py +145 -0
- ivoryos/routes/design/templates/components/action_form.html +53 -0
- ivoryos/routes/design/templates/components/actions_panel.html +25 -0
- ivoryos/routes/design/templates/components/autofill_toggle.html +10 -0
- ivoryos/routes/design/templates/components/canvas.html +5 -0
- ivoryos/routes/design/templates/components/canvas_footer.html +9 -0
- ivoryos/routes/design/templates/components/canvas_header.html +75 -0
- ivoryos/routes/design/templates/components/canvas_main.html +34 -0
- ivoryos/routes/design/templates/components/deck_selector.html +10 -0
- ivoryos/routes/design/templates/components/edit_action_form.html +38 -0
- ivoryos/routes/design/templates/components/instruments_panel.html +66 -0
- ivoryos/routes/design/templates/components/modals/drop_modal.html +17 -0
- ivoryos/routes/design/templates/components/modals/json_modal.html +22 -0
- ivoryos/routes/design/templates/components/modals/new_script_modal.html +17 -0
- ivoryos/routes/design/templates/components/modals/rename_modal.html +23 -0
- ivoryos/routes/design/templates/components/modals/saveas_modal.html +27 -0
- ivoryos/routes/design/templates/components/modals.html +6 -0
- ivoryos/routes/design/templates/components/python_code_overlay.html +39 -0
- ivoryos/routes/design/templates/components/sidebar.html +15 -0
- ivoryos/routes/design/templates/components/text_to_code_panel.html +20 -0
- ivoryos/routes/design/templates/experiment_builder.html +41 -0
- ivoryos/routes/execute/__init__.py +0 -0
- ivoryos/routes/execute/execute.py +317 -0
- ivoryos/routes/execute/execute_file.py +78 -0
- ivoryos/routes/execute/templates/components/error_modal.html +20 -0
- ivoryos/routes/execute/templates/components/logging_panel.html +31 -0
- ivoryos/routes/execute/templates/components/progress_panel.html +27 -0
- ivoryos/routes/execute/templates/components/run_panel.html +9 -0
- ivoryos/routes/execute/templates/components/run_tabs.html +17 -0
- ivoryos/routes/execute/templates/components/tab_bayesian.html +398 -0
- ivoryos/routes/execute/templates/components/tab_configuration.html +98 -0
- ivoryos/routes/execute/templates/components/tab_repeat.html +14 -0
- ivoryos/routes/execute/templates/experiment_run.html +294 -0
- ivoryos/routes/library/__init__.py +0 -0
- ivoryos/routes/library/library.py +159 -0
- ivoryos/routes/{database/templates/database/scripts_database.html → library/templates/library.html} +30 -22
- ivoryos/routes/main/main.py +1 -1
- ivoryos/routes/main/templates/{main/home.html → home.html} +4 -4
- ivoryos/socket_handlers.py +52 -0
- ivoryos/static/js/action_handlers.js +213 -0
- ivoryos/static/js/db_delete.js +23 -0
- ivoryos/static/js/script_metadata.js +39 -0
- ivoryos/static/js/sortable_design.js +89 -56
- ivoryos/static/js/ui_state.js +113 -0
- ivoryos/templates/base.html +4 -4
- ivoryos/utils/bo_campaign.py +179 -3
- ivoryos/utils/db_models.py +14 -5
- ivoryos/utils/form.py +5 -9
- ivoryos/utils/global_config.py +13 -1
- ivoryos/utils/py_to_json.py +225 -0
- ivoryos/utils/script_runner.py +49 -7
- ivoryos/utils/serilize.py +203 -0
- ivoryos/utils/task_runner.py +4 -1
- ivoryos/version.py +1 -1
- {ivoryos-1.0.9.dist-info → ivoryos-1.2.0.dist-info}/METADATA +5 -8
- ivoryos-1.2.0.dist-info/RECORD +105 -0
- ivoryos/routes/control/templates/control/controllers.html +0 -78
- ivoryos/routes/control/templates/control/controllers_home.html +0 -55
- ivoryos/routes/control/templates/control/controllers_new.html +0 -89
- ivoryos/routes/database/database.py +0 -306
- ivoryos/routes/database/templates/database/step_card.html +0 -7
- ivoryos/routes/design/templates/design/experiment_builder.html +0 -521
- ivoryos/routes/design/templates/design/experiment_run.html +0 -558
- ivoryos-1.0.9.dist-info/RECORD +0 -61
- /ivoryos/routes/auth/templates/{auth/login.html → login.html} +0 -0
- /ivoryos/routes/auth/templates/{auth/signup.html → signup.html} +0 -0
- /ivoryos/routes/{database → data}/__init__.py +0 -0
- /ivoryos/routes/main/templates/{main/help.html → help.html} +0 -0
- {ivoryos-1.0.9.dist-info → ivoryos-1.2.0.dist-info}/LICENSE +0 -0
- {ivoryos-1.0.9.dist-info → ivoryos-1.2.0.dist-info}/WHEEL +0 -0
- {ivoryos-1.0.9.dist-info → ivoryos-1.2.0.dist-info}/top_level.txt +0 -0
ivoryos/utils/bo_campaign.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
from typing import Dict, Any
|
|
2
|
+
import re
|
|
1
3
|
from ivoryos.utils.utils import install_and_import
|
|
2
4
|
|
|
3
5
|
|
|
4
|
-
def ax_init_form(data, arg_types):
|
|
6
|
+
def ax_init_form(data, arg_types, previous_data_len=0):
|
|
5
7
|
"""
|
|
6
8
|
create Ax campaign from the web form input
|
|
7
9
|
:param data:
|
|
@@ -9,7 +11,11 @@ def ax_init_form(data, arg_types):
|
|
|
9
11
|
install_and_import("ax", "ax-platform")
|
|
10
12
|
parameter, objectives = ax_wrapper(data, arg_types)
|
|
11
13
|
from ax.service.ax_client import AxClient
|
|
12
|
-
|
|
14
|
+
if previous_data_len > 0:
|
|
15
|
+
gs = exisitng_data_gs(previous_data_len)
|
|
16
|
+
ax_client = AxClient(generation_strategy=gs)
|
|
17
|
+
else:
|
|
18
|
+
ax_client = AxClient()
|
|
13
19
|
ax_client.create_experiment(parameter, objectives=objectives)
|
|
14
20
|
return ax_client
|
|
15
21
|
|
|
@@ -84,4 +90,174 @@ def ax_init_opc(bo_args):
|
|
|
84
90
|
bo_args["objectives"] = objectives_formatted
|
|
85
91
|
ax_client.create_experiment(**bo_args)
|
|
86
92
|
|
|
87
|
-
return ax_client
|
|
93
|
+
return ax_client
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def exisitng_data_gs(data_len):
|
|
97
|
+
"""
|
|
98
|
+
temporal generation strategy for existing data
|
|
99
|
+
"""
|
|
100
|
+
from ax.generation_strategy.generation_node import GenerationStep
|
|
101
|
+
from ax.generation_strategy.generation_strategy import GenerationStrategy
|
|
102
|
+
from ax.modelbridge.registry import Generators
|
|
103
|
+
if data_len > 4:
|
|
104
|
+
gs = GenerationStrategy(
|
|
105
|
+
steps=[
|
|
106
|
+
GenerationStep(
|
|
107
|
+
model=Generators.BOTORCH_MODULAR,
|
|
108
|
+
num_trials=-1,
|
|
109
|
+
max_parallelism=3,
|
|
110
|
+
),
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
gs = GenerationStrategy(
|
|
115
|
+
steps=[
|
|
116
|
+
GenerationStep(
|
|
117
|
+
model=Generators.SOBOL,
|
|
118
|
+
num_trials=5-data_len, # how many sobol trials to perform (rule of thumb: 2 * number of params)
|
|
119
|
+
max_parallelism=5,
|
|
120
|
+
model_kwargs={"seed": 999},
|
|
121
|
+
),
|
|
122
|
+
GenerationStep(
|
|
123
|
+
model=Generators.BOTORCH_MODULAR,
|
|
124
|
+
num_trials=-1,
|
|
125
|
+
max_parallelism=3,
|
|
126
|
+
),
|
|
127
|
+
]
|
|
128
|
+
)
|
|
129
|
+
return gs
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def parse_optimization_form(form_data: Dict[str, str]):
|
|
133
|
+
"""
|
|
134
|
+
Parse dynamic form data into structured optimization configuration.
|
|
135
|
+
|
|
136
|
+
Expected form field patterns:
|
|
137
|
+
- Objectives: {name}_min, {name}_weight
|
|
138
|
+
- Parameters: {name}_type, {name}_min, {name}_max, {name}_choices, {name}_value_type
|
|
139
|
+
- Config: step{n}_model, step{n}_num_samples
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
objectives = []
|
|
143
|
+
parameters = []
|
|
144
|
+
config = {}
|
|
145
|
+
|
|
146
|
+
# Track processed field names to avoid duplicates
|
|
147
|
+
processed_objectives = set()
|
|
148
|
+
processed_parameters = set()
|
|
149
|
+
|
|
150
|
+
# Parse objectives
|
|
151
|
+
for field_name, value in form_data.items():
|
|
152
|
+
if field_name.endswith('_min') and value:
|
|
153
|
+
# Extract objective name
|
|
154
|
+
obj_name = field_name.replace('_min', '')
|
|
155
|
+
if obj_name in processed_objectives:
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
# Check if corresponding weight exists
|
|
159
|
+
weight_field = f"{obj_name}_weight"
|
|
160
|
+
if weight_field in form_data and form_data[weight_field]:
|
|
161
|
+
objectives.append({
|
|
162
|
+
"name": obj_name,
|
|
163
|
+
"minimize": value == "minimize",
|
|
164
|
+
"weight": float(form_data[weight_field])
|
|
165
|
+
})
|
|
166
|
+
else:
|
|
167
|
+
objectives.append({
|
|
168
|
+
"name": obj_name,
|
|
169
|
+
"minimize": value == "minimize",
|
|
170
|
+
})
|
|
171
|
+
processed_objectives.add(obj_name)
|
|
172
|
+
|
|
173
|
+
# Parse parameters
|
|
174
|
+
for field_name, value in form_data.items():
|
|
175
|
+
if field_name.endswith('_type') and value:
|
|
176
|
+
# Extract parameter name
|
|
177
|
+
param_name = field_name.replace('_type', '')
|
|
178
|
+
if param_name in processed_parameters:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
parameter = {
|
|
182
|
+
"name": param_name,
|
|
183
|
+
"type": value
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Get value type (default to float)
|
|
187
|
+
value_type_field = f"{param_name}_value_type"
|
|
188
|
+
value_type = form_data.get(value_type_field, "float")
|
|
189
|
+
parameter["value_type"] = value_type
|
|
190
|
+
|
|
191
|
+
# Handle different parameter types
|
|
192
|
+
if value == "range":
|
|
193
|
+
min_field = f"{param_name}_min"
|
|
194
|
+
max_field = f"{param_name}_max"
|
|
195
|
+
|
|
196
|
+
if min_field in form_data and max_field in form_data:
|
|
197
|
+
min_val = form_data[min_field]
|
|
198
|
+
max_val = form_data[max_field]
|
|
199
|
+
|
|
200
|
+
if min_val and max_val:
|
|
201
|
+
# Convert based on value_type
|
|
202
|
+
if value_type == "int":
|
|
203
|
+
bounds = [int(min_val), int(max_val)]
|
|
204
|
+
elif value_type == "float":
|
|
205
|
+
bounds = [float(min_val), float(max_val)]
|
|
206
|
+
else: # string
|
|
207
|
+
bounds = [str(min_val), str(max_val)]
|
|
208
|
+
|
|
209
|
+
parameter["bounds"] = bounds
|
|
210
|
+
|
|
211
|
+
elif value == "choice":
|
|
212
|
+
choices_field = f"{param_name}_choices"
|
|
213
|
+
if choices_field in form_data and form_data[choices_field]:
|
|
214
|
+
# Split choices by comma and clean whitespace
|
|
215
|
+
choices = [choice.strip() for choice in form_data[choices_field].split(',')]
|
|
216
|
+
|
|
217
|
+
# Convert choices based on value_type
|
|
218
|
+
if value_type == "int":
|
|
219
|
+
choices = [int(choice) for choice in choices if choice.isdigit()]
|
|
220
|
+
elif value_type == "float":
|
|
221
|
+
choices = [float(choice) for choice in choices if
|
|
222
|
+
choice.replace('.', '').replace('-', '').isdigit()]
|
|
223
|
+
# For string, keep as is
|
|
224
|
+
|
|
225
|
+
parameter["bounds"] = choices
|
|
226
|
+
|
|
227
|
+
elif value == "fixed":
|
|
228
|
+
fixed_field = f"{param_name}_value"
|
|
229
|
+
if fixed_field in form_data and form_data[fixed_field]:
|
|
230
|
+
fixed_val = form_data[fixed_field]
|
|
231
|
+
|
|
232
|
+
# Convert based on value_type
|
|
233
|
+
if value_type == "int":
|
|
234
|
+
parameter["value"] = int(fixed_val)
|
|
235
|
+
elif value_type == "float":
|
|
236
|
+
parameter["value"] = float(fixed_val)
|
|
237
|
+
else:
|
|
238
|
+
parameter["value"] = str(fixed_val)
|
|
239
|
+
|
|
240
|
+
parameters.append(parameter)
|
|
241
|
+
processed_parameters.add(param_name)
|
|
242
|
+
|
|
243
|
+
# Parse configuration steps
|
|
244
|
+
step_pattern = re.compile(r'step(\d+)_(.+)')
|
|
245
|
+
steps = {}
|
|
246
|
+
|
|
247
|
+
for field_name, value in form_data.items():
|
|
248
|
+
match = step_pattern.match(field_name)
|
|
249
|
+
if match and value:
|
|
250
|
+
step_num = int(match.group(1))
|
|
251
|
+
step_attr = match.group(2)
|
|
252
|
+
step_key = f"step_{step_num}"
|
|
253
|
+
|
|
254
|
+
if step_key not in steps:
|
|
255
|
+
steps[step_key] = {}
|
|
256
|
+
|
|
257
|
+
# Convert num_samples to int if it's a number field
|
|
258
|
+
if step_attr == "num_samples":
|
|
259
|
+
steps[step_key][step_attr] = int(value)
|
|
260
|
+
else:
|
|
261
|
+
steps[step_key][step_attr] = value
|
|
262
|
+
|
|
263
|
+
return parameters, objectives, steps
|
ivoryos/utils/db_models.py
CHANGED
|
@@ -265,11 +265,20 @@ class Script(db.Model):
|
|
|
265
265
|
output_variables: Dict[str, str] = self.get_variables()
|
|
266
266
|
# print(output_variables)
|
|
267
267
|
for key, value in kwargs.items():
|
|
268
|
-
if
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
268
|
+
if isinstance(value, str):
|
|
269
|
+
if value in output_variables:
|
|
270
|
+
var_type = output_variables[value]
|
|
271
|
+
kwargs[key] = {value: var_type}
|
|
272
|
+
elif value.startswith("#"):
|
|
273
|
+
kwargs[key] = f"#{self.validate_function_name(value[1:])}"
|
|
274
|
+
else:
|
|
275
|
+
# attempt to convert to numerical or bool value for args with no type hint
|
|
276
|
+
try:
|
|
277
|
+
converted = ast.literal_eval(value)
|
|
278
|
+
if isinstance(converted, (int, float, bool)):
|
|
279
|
+
kwargs[key] = converted
|
|
280
|
+
except (ValueError, SyntaxError):
|
|
281
|
+
pass
|
|
273
282
|
return kwargs
|
|
274
283
|
|
|
275
284
|
def add_logic_action(self, logic_type: str, statement, insert_position=None):
|
ivoryos/utils/form.py
CHANGED
|
@@ -214,11 +214,6 @@ class FlexibleEnumField(StringField):
|
|
|
214
214
|
f"Invalid choice: '{key}'. Must match one of {list(self.enum_class.__members__.keys())}")
|
|
215
215
|
|
|
216
216
|
|
|
217
|
-
def format_name(name):
|
|
218
|
-
"""Converts 'example_name' to 'Example Name'."""
|
|
219
|
-
name = name.split(".")[-1]
|
|
220
|
-
text = ' '.join(word for word in name.split('_'))
|
|
221
|
-
return text.capitalize()
|
|
222
217
|
|
|
223
218
|
def parse_annotation(annotation):
|
|
224
219
|
"""
|
|
@@ -264,7 +259,7 @@ def create_form_for_method(method, autofill, script=None, design=True):
|
|
|
264
259
|
for param in sig.parameters.values():
|
|
265
260
|
if param.name == 'self':
|
|
266
261
|
continue
|
|
267
|
-
formatted_param_name = format_name(param.name)
|
|
262
|
+
# formatted_param_name = format_name(param.name)
|
|
268
263
|
|
|
269
264
|
default_value = None
|
|
270
265
|
if autofill:
|
|
@@ -277,7 +272,7 @@ def create_form_for_method(method, autofill, script=None, design=True):
|
|
|
277
272
|
default_value = param.default
|
|
278
273
|
|
|
279
274
|
field_kwargs = {
|
|
280
|
-
"label":
|
|
275
|
+
"label": param.name,
|
|
281
276
|
"default": default_value,
|
|
282
277
|
"validators": [InputRequired()] if param.default is param.empty else [Optional()],
|
|
283
278
|
**({"script": script} if (autofill or design) else {})
|
|
@@ -321,6 +316,7 @@ def create_add_form(attr, attr_name, autofill: bool, script=None, design: bool =
|
|
|
321
316
|
"""
|
|
322
317
|
signature = attr.get('signature', {})
|
|
323
318
|
docstring = attr.get('docstring', "")
|
|
319
|
+
# print(signature, docstring)
|
|
324
320
|
dynamic_form = create_form_for_method(signature, autofill, script, design)
|
|
325
321
|
if design:
|
|
326
322
|
return_value = StringField(label='Save value as', render_kw={"placeholder": "Optional"})
|
|
@@ -395,12 +391,12 @@ def create_form_from_action(action: dict, script=None, design=True):
|
|
|
395
391
|
}
|
|
396
392
|
|
|
397
393
|
for name, param_type in arg_types.items():
|
|
398
|
-
formatted_param_name = format_name(name)
|
|
394
|
+
# formatted_param_name = format_name(name)
|
|
399
395
|
value = args.get(name, "")
|
|
400
396
|
if type(value) is dict:
|
|
401
397
|
value = next(iter(value))
|
|
402
398
|
field_kwargs = {
|
|
403
|
-
"label":
|
|
399
|
+
"label": name,
|
|
404
400
|
"default": f'{value}',
|
|
405
401
|
"validators": [InputRequired()],
|
|
406
402
|
**({"script": script})
|
ivoryos/utils/global_config.py
CHANGED
|
@@ -15,6 +15,7 @@ class GlobalConfig:
|
|
|
15
15
|
cls._instance._deck_snapshot = {}
|
|
16
16
|
cls._instance._runner_lock = threading.Lock()
|
|
17
17
|
cls._instance._runner_status = None
|
|
18
|
+
cls._instance._optimizers = {}
|
|
18
19
|
return cls._instance
|
|
19
20
|
|
|
20
21
|
@property
|
|
@@ -84,4 +85,15 @@ class GlobalConfig:
|
|
|
84
85
|
|
|
85
86
|
@runner_status.setter
|
|
86
87
|
def runner_status(self, value):
|
|
87
|
-
self._runner_status = value
|
|
88
|
+
self._runner_status = value
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def optimizers(self):
|
|
92
|
+
return self._optimizers
|
|
93
|
+
|
|
94
|
+
@optimizers.setter
|
|
95
|
+
def optimizers(self, value):
|
|
96
|
+
if isinstance(value, dict):
|
|
97
|
+
self._optimizers = value
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError("Optimizers must be a dictionary.")
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
def generate_uuid():
|
|
6
|
+
return int(str(uuid.uuid4().int)[:15])
|
|
7
|
+
|
|
8
|
+
def infer_type(value):
|
|
9
|
+
if isinstance(value, bool):
|
|
10
|
+
return "bool"
|
|
11
|
+
elif isinstance(value, int):
|
|
12
|
+
return "int"
|
|
13
|
+
elif isinstance(value, float):
|
|
14
|
+
return "float"
|
|
15
|
+
elif isinstance(value, str):
|
|
16
|
+
return "str"
|
|
17
|
+
elif isinstance(value, (ast.Name, str)) and str(value).startswith("#"):
|
|
18
|
+
return "float" # default fallback for variables
|
|
19
|
+
else:
|
|
20
|
+
return "unknown"
|
|
21
|
+
|
|
22
|
+
def convert_to_cards(source_code: str):
|
|
23
|
+
tree = ast.parse(source_code)
|
|
24
|
+
cards = []
|
|
25
|
+
card_id = 1
|
|
26
|
+
block_stack = [] # to track control flow UUIDs
|
|
27
|
+
|
|
28
|
+
def new_id():
|
|
29
|
+
nonlocal card_id
|
|
30
|
+
val = card_id
|
|
31
|
+
card_id += 1
|
|
32
|
+
return val
|
|
33
|
+
|
|
34
|
+
def add_card(card):
|
|
35
|
+
cards.append(card)
|
|
36
|
+
|
|
37
|
+
def is_supported_assignment(node):
|
|
38
|
+
return (
|
|
39
|
+
isinstance(node.targets[0], ast.Name) and
|
|
40
|
+
isinstance(node.value, (ast.Constant, ast.Num, ast.Str, ast.NameConstant))
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
class CardVisitor(ast.NodeVisitor):
|
|
44
|
+
def visit_FunctionDef(self, node):
|
|
45
|
+
self.defined_types = {
|
|
46
|
+
arg.arg: ast.unparse(arg.annotation) if arg.annotation else "float"
|
|
47
|
+
for arg in node.args.args
|
|
48
|
+
}
|
|
49
|
+
for stmt in node.body:
|
|
50
|
+
self.visit(stmt)
|
|
51
|
+
|
|
52
|
+
def visit_If(self, node):
|
|
53
|
+
uuid_ = generate_uuid()
|
|
54
|
+
block_stack.append(("if", uuid_))
|
|
55
|
+
|
|
56
|
+
add_card({
|
|
57
|
+
"action": "if",
|
|
58
|
+
"arg_types": {"statement": ""},
|
|
59
|
+
"args": {"statement": ast.unparse(node.test)},
|
|
60
|
+
"id": new_id(),
|
|
61
|
+
"instrument": "if",
|
|
62
|
+
"return": "",
|
|
63
|
+
"uuid": uuid_
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
for stmt in node.body:
|
|
67
|
+
self.visit(stmt)
|
|
68
|
+
|
|
69
|
+
if node.orelse:
|
|
70
|
+
add_card({
|
|
71
|
+
"action": "else",
|
|
72
|
+
"args": {},
|
|
73
|
+
"id": new_id(),
|
|
74
|
+
"instrument": "if",
|
|
75
|
+
"return": "",
|
|
76
|
+
"uuid": uuid_
|
|
77
|
+
})
|
|
78
|
+
for stmt in node.orelse:
|
|
79
|
+
self.visit(stmt)
|
|
80
|
+
|
|
81
|
+
_, block_uuid = block_stack.pop()
|
|
82
|
+
add_card({
|
|
83
|
+
"action": "endif",
|
|
84
|
+
"args": {},
|
|
85
|
+
"id": new_id(),
|
|
86
|
+
"instrument": "if",
|
|
87
|
+
"return": "",
|
|
88
|
+
"uuid": block_uuid
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
def visit_While(self, node):
|
|
92
|
+
uuid_ = generate_uuid()
|
|
93
|
+
block_stack.append(("while", uuid_))
|
|
94
|
+
|
|
95
|
+
add_card({
|
|
96
|
+
"action": "while",
|
|
97
|
+
"arg_types": {"statement": ""},
|
|
98
|
+
"args": {"statement": ast.unparse(node.test)},
|
|
99
|
+
"id": new_id(),
|
|
100
|
+
"instrument": "while",
|
|
101
|
+
"return": "",
|
|
102
|
+
"uuid": uuid_
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
for stmt in node.body:
|
|
106
|
+
self.visit(stmt)
|
|
107
|
+
|
|
108
|
+
_, block_uuid = block_stack.pop()
|
|
109
|
+
add_card({
|
|
110
|
+
"action": "endwhile",
|
|
111
|
+
"args": {},
|
|
112
|
+
"id": new_id(),
|
|
113
|
+
"instrument": "while",
|
|
114
|
+
"return": "",
|
|
115
|
+
"uuid": block_uuid
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
def visit_Assign(self, node):
|
|
119
|
+
if is_supported_assignment(node):
|
|
120
|
+
var_name = node.targets[0].id
|
|
121
|
+
value = node.value.value
|
|
122
|
+
add_card({
|
|
123
|
+
"action": var_name,
|
|
124
|
+
"arg_types": {"statement": infer_type(value)},
|
|
125
|
+
"args": {"statement": value},
|
|
126
|
+
"id": new_id(),
|
|
127
|
+
"instrument": "variable",
|
|
128
|
+
"return": "",
|
|
129
|
+
"uuid": generate_uuid()
|
|
130
|
+
})
|
|
131
|
+
elif isinstance(node.value, ast.Call):
|
|
132
|
+
self.handle_call(node.value, ret_var=node.targets[0].id)
|
|
133
|
+
|
|
134
|
+
def visit_Expr(self, node):
|
|
135
|
+
if isinstance(node.value, ast.Call):
|
|
136
|
+
self.handle_call(node.value)
|
|
137
|
+
|
|
138
|
+
def handle_call(self, node, ret_var=""):
|
|
139
|
+
func_parts = []
|
|
140
|
+
f = node.func
|
|
141
|
+
while isinstance(f, ast.Attribute):
|
|
142
|
+
func_parts.insert(0, f.attr)
|
|
143
|
+
f = f.value
|
|
144
|
+
if isinstance(f, ast.Name):
|
|
145
|
+
func_parts.insert(0, f.id)
|
|
146
|
+
if not func_parts or not func_parts[0].startswith("deck"):
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
instrument = ".".join(func_parts[:-1])
|
|
150
|
+
action = func_parts[-1]
|
|
151
|
+
|
|
152
|
+
args = {}
|
|
153
|
+
arg_types = {}
|
|
154
|
+
|
|
155
|
+
for kw in node.keywords:
|
|
156
|
+
if kw.arg is None and isinstance(kw.value, ast.Dict):
|
|
157
|
+
for k_node, v_node in zip(kw.value.keys, kw.value.values):
|
|
158
|
+
key = k_node.s if isinstance(k_node, ast.Constant) else ast.unparse(k_node)
|
|
159
|
+
if isinstance(v_node, ast.Constant):
|
|
160
|
+
value = v_node.value
|
|
161
|
+
elif isinstance(v_node, ast.Name):
|
|
162
|
+
value = f"#{v_node.id}"
|
|
163
|
+
else:
|
|
164
|
+
value = ast.unparse(v_node)
|
|
165
|
+
args[key] = value
|
|
166
|
+
arg_types[key] = infer_type(value)
|
|
167
|
+
else:
|
|
168
|
+
if isinstance(kw.value, ast.Constant):
|
|
169
|
+
value = kw.value.value
|
|
170
|
+
elif isinstance(kw.value, ast.Name):
|
|
171
|
+
value = f"#{kw.value.id}"
|
|
172
|
+
else:
|
|
173
|
+
value = ast.unparse(kw.value)
|
|
174
|
+
args[kw.arg] = value
|
|
175
|
+
arg_types[kw.arg] = (
|
|
176
|
+
self.defined_types.get(kw.value.id, "float")
|
|
177
|
+
if isinstance(kw.value, ast.Name)
|
|
178
|
+
else infer_type(value)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
add_card({
|
|
182
|
+
"action": action,
|
|
183
|
+
"arg_types": arg_types,
|
|
184
|
+
"args": args,
|
|
185
|
+
"id": new_id(),
|
|
186
|
+
"instrument": instrument,
|
|
187
|
+
"return": ret_var,
|
|
188
|
+
"uuid": generate_uuid()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
CardVisitor().visit(tree)
|
|
192
|
+
return cards
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
test = '''def workflow_dynamic(solid_amount_mg, methanol_amount_ml):
|
|
197
|
+
"""
|
|
198
|
+
SDL workflow: dose solid, add methanol, equilibrate, and analyze
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
solid_amount_mg (float): Amount of solid to dose in mg
|
|
202
|
+
methanol_amount_ml (float): Amount of methanol to dose in ml
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
dict: Results containing analysis data
|
|
206
|
+
"""
|
|
207
|
+
# Step 1: Dose solid material
|
|
208
|
+
deck.sdl.dose_solid(amount_in_mg=solid_amount_mg)
|
|
209
|
+
|
|
210
|
+
# Step 2: Add methanol solvent
|
|
211
|
+
deck.sdl.dose_solvent(solvent_name='Methanol', amount_in_ml=methanol_amount_ml)
|
|
212
|
+
|
|
213
|
+
# Step 3: Equilibrate at room temperature (assuming ~23°C) for 20 seconds
|
|
214
|
+
deck.sdl.equilibrate(temp=23.0, duration=20.0)
|
|
215
|
+
|
|
216
|
+
# Step 4: Analyze the sample
|
|
217
|
+
analysis_results = deck.sdl.analyze(param_1=1, param_2=2)
|
|
218
|
+
|
|
219
|
+
# Brief pause for system stability
|
|
220
|
+
time.sleep(1.0)
|
|
221
|
+
|
|
222
|
+
# Return only analysis results
|
|
223
|
+
return {'analysis_results': analysis_results}
|
|
224
|
+
'''
|
|
225
|
+
print(json.dumps(convert_to_cards(test)))
|
ivoryos/utils/script_runner.py
CHANGED
|
@@ -62,11 +62,11 @@ class ScriptRunner:
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def run_script(self, script, repeat_count=1, run_name=None, logger=None, socketio=None, config=None, bo_args=None,
|
|
65
|
-
output_path="", compiled=False, current_app=None):
|
|
65
|
+
output_path="", compiled=False, current_app=None, history=None, optimizer=None):
|
|
66
66
|
global deck
|
|
67
67
|
if deck is None:
|
|
68
68
|
deck = global_config.deck
|
|
69
|
-
|
|
69
|
+
print("history", history)
|
|
70
70
|
if self.current_app is None:
|
|
71
71
|
self.current_app = current_app
|
|
72
72
|
# time.sleep(1) # Optional: may help ensure deck readiness
|
|
@@ -81,7 +81,8 @@ class ScriptRunner:
|
|
|
81
81
|
|
|
82
82
|
thread = threading.Thread(
|
|
83
83
|
target=self._run_with_stop_check,
|
|
84
|
-
args=(script, repeat_count, run_name, logger, socketio, config, bo_args, output_path, current_app, compiled
|
|
84
|
+
args=(script, repeat_count, run_name, logger, socketio, config, bo_args, output_path, current_app, compiled,
|
|
85
|
+
history, optimizer)
|
|
85
86
|
)
|
|
86
87
|
thread.start()
|
|
87
88
|
return thread
|
|
@@ -182,7 +183,7 @@ class ScriptRunner:
|
|
|
182
183
|
return exec_locals # Return the 'results' variable
|
|
183
184
|
|
|
184
185
|
def _run_with_stop_check(self, script: Script, repeat_count: int, run_name: str, logger, socketio, config, bo_args,
|
|
185
|
-
output_path, current_app, compiled):
|
|
186
|
+
output_path, current_app, compiled, history=None, optimizer=None):
|
|
186
187
|
time.sleep(1)
|
|
187
188
|
# _func_str = script.compile()
|
|
188
189
|
# step_list_dict: dict = script.convert_to_lines(_func_str)
|
|
@@ -205,7 +206,8 @@ class ScriptRunner:
|
|
|
205
206
|
# Run "script" section multiple times
|
|
206
207
|
if repeat_count:
|
|
207
208
|
self._run_repeat_section(repeat_count, arg_type, bo_args, output_list, script,
|
|
208
|
-
run_name, return_list, compiled, logger, socketio,
|
|
209
|
+
run_name, return_list, compiled, logger, socketio,
|
|
210
|
+
history, output_path, run_id=run_id, optimizer=optimizer)
|
|
209
211
|
elif config:
|
|
210
212
|
self._run_config_section(config, arg_type, output_list, script, run_name, logger,
|
|
211
213
|
socketio, run_id=run_id, compiled=compiled)
|
|
@@ -262,13 +264,36 @@ class ScriptRunner:
|
|
|
262
264
|
output_list.append(output)
|
|
263
265
|
|
|
264
266
|
def _run_repeat_section(self, repeat_count, arg_types, bo_args, output_list, script, run_name, return_list, compiled,
|
|
265
|
-
logger, socketio, run_id):
|
|
267
|
+
logger, socketio, history, output_path, run_id, optimizer=None):
|
|
266
268
|
if bo_args:
|
|
267
269
|
logger.info('Initializing optimizer...')
|
|
268
270
|
if compiled:
|
|
269
271
|
ax_client = bo_campaign.ax_init_opc(bo_args)
|
|
270
272
|
else:
|
|
271
|
-
|
|
273
|
+
if history:
|
|
274
|
+
import pandas as pd
|
|
275
|
+
file_path = os.path.join(output_path, history)
|
|
276
|
+
previous_runs = pd.read_csv(file_path).to_dict(orient='records')
|
|
277
|
+
ax_client = bo_campaign.ax_init_form(bo_args, arg_types, len(previous_runs))
|
|
278
|
+
for row in previous_runs:
|
|
279
|
+
parameter = {key: value for key, value in row.items() if key in arg_types.keys()}
|
|
280
|
+
raw_data = {key: value for key, value in row.items() if key in return_list}
|
|
281
|
+
_, trial_index = ax_client.attach_trial(parameter)
|
|
282
|
+
ax_client.complete_trial(trial_index=trial_index, raw_data=raw_data)
|
|
283
|
+
output_list.append(row)
|
|
284
|
+
else:
|
|
285
|
+
ax_client = bo_campaign.ax_init_form(bo_args, arg_types)
|
|
286
|
+
elif optimizer and history:
|
|
287
|
+
import pandas as pd
|
|
288
|
+
file_path = os.path.join(output_path, history)
|
|
289
|
+
|
|
290
|
+
previous_runs = pd.read_csv(file_path)
|
|
291
|
+
optimizer.append_existing_data(previous_runs)
|
|
292
|
+
for row in previous_runs:
|
|
293
|
+
output_list.append(row)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
272
297
|
for i_progress in range(int(repeat_count)):
|
|
273
298
|
if self.stop_pending_event.is_set():
|
|
274
299
|
logger.info(f'Stopping execution during {run_name}: {i_progress + 1}/{int(repeat_count)}')
|
|
@@ -290,6 +315,17 @@ class ScriptRunner:
|
|
|
290
315
|
except Exception as e:
|
|
291
316
|
logger.info(f'Optimization error: {e}')
|
|
292
317
|
break
|
|
318
|
+
elif optimizer:
|
|
319
|
+
try:
|
|
320
|
+
parameters = optimizer.suggest(1)
|
|
321
|
+
logger.info(f'Output value: {parameters}')
|
|
322
|
+
output = self.exec_steps(script, "script", logger, socketio, run_id, i_progress, **parameters)
|
|
323
|
+
if output:
|
|
324
|
+
optimizer.observe(output)
|
|
325
|
+
output.update(parameters)
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.info(f'Optimization error: {e}')
|
|
328
|
+
break
|
|
293
329
|
else:
|
|
294
330
|
# fname = f"{run_name}_script"
|
|
295
331
|
# function = self.globals_dict[fname]
|
|
@@ -298,6 +334,12 @@ class ScriptRunner:
|
|
|
298
334
|
if output:
|
|
299
335
|
output_list.append(output)
|
|
300
336
|
logger.info(f'Output value: {output}')
|
|
337
|
+
|
|
338
|
+
if bo_args:
|
|
339
|
+
ax_client.save_to_json_file(os.path.join(output_path, f"{run_name}_ax_client.json"))
|
|
340
|
+
logger.info(
|
|
341
|
+
f'Optimization complete. Results saved to {os.path.join(output_path, f"{run_name}_ax_client.json")}'
|
|
342
|
+
)
|
|
301
343
|
return output_list
|
|
302
344
|
|
|
303
345
|
@staticmethod
|