ivoryos 1.3.9__py3-none-any.whl → 1.4.1__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/optimizer/ax_optimizer.py +47 -25
- ivoryos/optimizer/base_optimizer.py +19 -1
- ivoryos/optimizer/baybe_optimizer.py +27 -17
- ivoryos/optimizer/nimo_optimizer.py +25 -16
- ivoryos/routes/data/data.py +27 -9
- ivoryos/routes/data/templates/components/step_card.html +47 -11
- ivoryos/routes/data/templates/workflow_view.html +14 -5
- ivoryos/routes/design/design.py +31 -1
- ivoryos/routes/design/design_step.py +2 -1
- ivoryos/routes/design/templates/components/edit_action_form.html +16 -3
- ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
- ivoryos/routes/design/templates/experiment_builder.html +1 -0
- ivoryos/routes/execute/execute.py +71 -13
- ivoryos/routes/execute/templates/components/logging_panel.html +50 -25
- ivoryos/routes/execute/templates/components/run_tabs.html +45 -2
- ivoryos/routes/execute/templates/components/tab_bayesian.html +447 -325
- ivoryos/routes/execute/templates/components/tab_configuration.html +303 -18
- ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
- ivoryos/routes/execute/templates/experiment_run.html +0 -264
- ivoryos/socket_handlers.py +1 -1
- ivoryos/static/js/action_handlers.js +252 -118
- ivoryos/static/js/sortable_design.js +1 -0
- ivoryos/utils/bo_campaign.py +17 -16
- ivoryos/utils/db_models.py +122 -18
- ivoryos/utils/decorators.py +1 -0
- ivoryos/utils/form.py +32 -12
- ivoryos/utils/nest_script.py +314 -0
- ivoryos/utils/script_runner.py +436 -143
- ivoryos/utils/utils.py +11 -1
- ivoryos/version.py +1 -1
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/METADATA +6 -4
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/RECORD +35 -34
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/WHEEL +0 -0
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/licenses/LICENSE +0 -0
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/top_level.txt +0 -0
ivoryos/utils/script_runner.py
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
import asyncio
|
|
3
3
|
import os
|
|
4
|
-
import csv
|
|
5
4
|
import threading
|
|
6
5
|
import time
|
|
7
6
|
from datetime import datetime
|
|
7
|
+
from typing import List, Dict, Any
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
8
10
|
|
|
9
11
|
from ivoryos.utils import utils, bo_campaign
|
|
10
12
|
from ivoryos.utils.db_models import Script, WorkflowRun, WorkflowStep, db, WorkflowPhase
|
|
11
13
|
from ivoryos.utils.global_config import GlobalConfig
|
|
12
14
|
from ivoryos.utils.decorators import BUILDING_BLOCKS
|
|
15
|
+
from ivoryos.utils.nest_script import validate_and_nest_control_flow
|
|
13
16
|
|
|
14
17
|
global_config = GlobalConfig()
|
|
15
18
|
global deck
|
|
@@ -32,6 +35,8 @@ def pause(reason="Human intervention required"):
|
|
|
32
35
|
|
|
33
36
|
class ScriptRunner:
|
|
34
37
|
def __init__(self, globals_dict=None):
|
|
38
|
+
self.logger = None
|
|
39
|
+
self.socketio = None
|
|
35
40
|
self.retry = False
|
|
36
41
|
if globals_dict is None:
|
|
37
42
|
globals_dict = globals()
|
|
@@ -74,13 +79,19 @@ class ScriptRunner:
|
|
|
74
79
|
"""Force stop everything, including ongoing tasks."""
|
|
75
80
|
self.stop_current_event.set()
|
|
76
81
|
self.abort_pending()
|
|
82
|
+
if self.lock.locked():
|
|
83
|
+
self.lock.release()
|
|
77
84
|
|
|
78
85
|
|
|
79
86
|
def run_script(self, script, repeat_count=1, run_name=None, logger=None, socketio=None, config=None, bo_args=None,
|
|
80
|
-
output_path="", compiled=False, current_app=None, history=None, optimizer=None
|
|
87
|
+
output_path="", compiled=False, current_app=None, history=None, optimizer=None, batch_mode=None,
|
|
88
|
+
batch_size=1, objectives=None):
|
|
89
|
+
self.socketio = socketio
|
|
90
|
+
self.logger = logger
|
|
81
91
|
global deck
|
|
82
92
|
if deck is None:
|
|
83
93
|
deck = global_config.deck
|
|
94
|
+
|
|
84
95
|
# print("history", history)
|
|
85
96
|
if self.current_app is None:
|
|
86
97
|
self.current_app = current_app
|
|
@@ -97,12 +108,14 @@ class ScriptRunner:
|
|
|
97
108
|
thread = threading.Thread(
|
|
98
109
|
target=self._run_with_stop_check,
|
|
99
110
|
args=(script, repeat_count, run_name, logger, socketio, config, bo_args, output_path, current_app, compiled,
|
|
100
|
-
history, optimizer)
|
|
111
|
+
history, optimizer, batch_mode, batch_size, objectives),
|
|
101
112
|
)
|
|
102
113
|
thread.start()
|
|
103
114
|
return thread
|
|
104
115
|
|
|
105
|
-
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def exec_steps(self, script, section_name, phase_id, kwargs_list=None, batch_size=1):
|
|
106
119
|
"""
|
|
107
120
|
Executes a function defined in a string line by line
|
|
108
121
|
:param func_str: The function as a string
|
|
@@ -131,112 +144,54 @@ class ScriptRunner:
|
|
|
131
144
|
# exec_globals = {"deck": deck, "time": time, "registered_workflows":registered_workflows} # Add required global objects
|
|
132
145
|
exec_globals.update(temp_connections)
|
|
133
146
|
|
|
134
|
-
# Inject all block categories
|
|
135
|
-
for category, data in BUILDING_BLOCKS.items():
|
|
136
|
-
for method_name, method in data.items():
|
|
137
|
-
exec_globals[method_name] = method["func"]
|
|
138
|
-
|
|
139
147
|
exec_locals = {} # Local execution scope
|
|
140
148
|
|
|
141
149
|
# Define function arguments manually in exec_locals
|
|
142
|
-
exec_locals.update(kwargs)
|
|
150
|
+
# exec_locals.update(kwargs)
|
|
143
151
|
index = 0
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
method_name=method_name,
|
|
172
|
-
start_time=datetime.now(),
|
|
173
|
-
)
|
|
174
|
-
db.session.add(step)
|
|
175
|
-
db.session.flush()
|
|
176
|
-
|
|
177
|
-
logger.info(f"Executing: {line}")
|
|
178
|
-
socketio.emit('execution', {'section': f"{section_name}-{index}"})
|
|
179
|
-
|
|
180
|
-
# if line.startswith("registered_workflows"):
|
|
181
|
-
# line = line.replace("registered_workflows.", "")
|
|
182
|
-
try:
|
|
183
|
-
if line.startswith("time.sleep("): # add safe sleep for time.sleep lines
|
|
184
|
-
duration_str = line.strip()[len("time.sleep("):-1]
|
|
185
|
-
duration = float(duration_str)
|
|
186
|
-
self.safe_sleep(duration)
|
|
152
|
+
if kwargs_list:
|
|
153
|
+
results = kwargs_list.copy()
|
|
154
|
+
else:
|
|
155
|
+
results = []
|
|
156
|
+
nest_script = validate_and_nest_control_flow(script.script_dict.get(section_name, []))
|
|
157
|
+
|
|
158
|
+
for step in nest_script:
|
|
159
|
+
action = step["action"]
|
|
160
|
+
instrument = step["instrument"]
|
|
161
|
+
action_id = step["id"]
|
|
162
|
+
if action == "if":
|
|
163
|
+
await self._execute_if_batched(step, results, phase_id=phase_id, step_index=action_id, section_name=section_name)
|
|
164
|
+
elif action == "repeat":
|
|
165
|
+
await self._execute_repeat_batched(step, results, phase_id=phase_id, step_index=action_id, section_name=section_name)
|
|
166
|
+
elif action == "while":
|
|
167
|
+
await self._execute_while_batched(step, results, phase_id=phase_id, step_index=action_id, section_name=section_name)
|
|
168
|
+
elif instrument == "variable":
|
|
169
|
+
await self._execute_variable_batched(step, results, phase_id=phase_id, step_index=action_id, section_name=section_name)
|
|
170
|
+
print("Variable executed", "current context", context)
|
|
171
|
+
else:
|
|
172
|
+
# Regular action - check if batch
|
|
173
|
+
if step.get("batch_action", False):
|
|
174
|
+
# Execute once for all samples
|
|
175
|
+
if results:
|
|
176
|
+
await self._execute_action_once(step, results[0], phase_id=phase_id, step_index=action_id, section_name=section_name)
|
|
177
|
+
else:
|
|
178
|
+
await self._execute_action_once(step, {}, phase_id=phase_id, step_index=action_id, section_name=section_name)
|
|
187
179
|
else:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
exec(async_code, exec_globals, exec_locals)
|
|
194
|
-
func = exec_locals.get("__async_exec_wrapper") or exec_globals.get("__async_exec_wrapper")
|
|
195
|
-
# Capture the return value from asyncio.run
|
|
196
|
-
result_locals = asyncio.run(func())
|
|
197
|
-
|
|
198
|
-
# Update exec_locals with the returned locals
|
|
199
|
-
exec_locals.update(result_locals)
|
|
200
|
-
|
|
201
|
-
|
|
180
|
+
# Execute for each sample
|
|
181
|
+
if results:
|
|
182
|
+
for context in results:
|
|
183
|
+
await self._execute_action(step, context, phase_id=phase_id, step_index=action_id, section_name=section_name)
|
|
184
|
+
self.pause_event.wait()
|
|
202
185
|
else:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
# return locals_dict
|
|
207
|
-
# exec(line, exec_globals, exec_locals)
|
|
208
|
-
# step.run_error = False
|
|
209
|
-
|
|
210
|
-
except HumanInterventionRequired as e:
|
|
211
|
-
logger.warning(f"Human intervention required: {e}")
|
|
212
|
-
socketio.emit('human_intervention', {'message': str(e)})
|
|
213
|
-
# Instead of auto-resume, explicitly stay paused until user action
|
|
214
|
-
# step.run_error = False
|
|
215
|
-
self.toggle_pause()
|
|
216
|
-
|
|
217
|
-
except Exception as e:
|
|
218
|
-
logger.error(f"Error during script execution: {e}")
|
|
219
|
-
socketio.emit('error', {'message': str(e)})
|
|
220
|
-
|
|
221
|
-
step.run_error = True
|
|
222
|
-
self.toggle_pause()
|
|
223
|
-
exec_locals.pop("__async_exec_wrapper", None)
|
|
224
|
-
step.end_time = datetime.now()
|
|
225
|
-
step.output = exec_locals
|
|
226
|
-
db.session.commit()
|
|
186
|
+
for _ in range(batch_size):
|
|
187
|
+
await self._execute_action(step, {}, phase_id=phase_id, step_index=action_id, section_name=section_name)
|
|
188
|
+
self.pause_event.wait()
|
|
227
189
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
# todo update script during the run
|
|
231
|
-
# _func_str = script.compile()
|
|
232
|
-
# step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
|
|
233
|
-
if not step.run_error or not self.retry:
|
|
234
|
-
index += 1
|
|
235
|
-
output = {key: value for key, value in exec_locals.items() if key in return_list}
|
|
236
|
-
return output # Return the 'results' variable
|
|
190
|
+
return results # Return the 'results' variable
|
|
237
191
|
|
|
238
192
|
def _run_with_stop_check(self, script: Script, repeat_count: int, run_name: str, logger, socketio, config, bo_args,
|
|
239
|
-
output_path, current_app, compiled, history=None, optimizer=None
|
|
193
|
+
output_path, current_app, compiled, history=None, optimizer=None, batch_mode=None,
|
|
194
|
+
batch_size=None, objectives=None):
|
|
240
195
|
time.sleep(1)
|
|
241
196
|
# _func_str = script.compile()
|
|
242
197
|
# step_list_dict: dict = script.convert_to_lines(_func_str)
|
|
@@ -255,26 +210,37 @@ class ScriptRunner:
|
|
|
255
210
|
db.session.commit()
|
|
256
211
|
|
|
257
212
|
try:
|
|
258
|
-
|
|
213
|
+
# if True:
|
|
259
214
|
global_config.runner_status = {"id":run_id, "type": "workflow"}
|
|
260
215
|
# Run "prep" section once
|
|
261
|
-
self._run_actions(script, section_name="prep", logger=logger, socketio=socketio, run_id=run_id)
|
|
216
|
+
asyncio.run(self._run_actions(script, section_name="prep", logger=logger, socketio=socketio, run_id=run_id))
|
|
262
217
|
output_list = []
|
|
263
218
|
_, arg_type = script.config("script")
|
|
264
219
|
_, return_list = script.config_return()
|
|
265
220
|
# Run "script" section multiple times
|
|
266
221
|
if repeat_count:
|
|
267
|
-
|
|
222
|
+
asyncio.run(
|
|
223
|
+
self._run_repeat_section(repeat_count, arg_type, bo_args, output_list, script,
|
|
268
224
|
run_name, return_list, compiled, logger, socketio,
|
|
269
|
-
history, output_path, run_id=run_id, optimizer=optimizer
|
|
225
|
+
history, output_path, run_id=run_id, optimizer=optimizer,
|
|
226
|
+
batch_mode=batch_mode, batch_size=batch_size, objectives=objectives)
|
|
227
|
+
)
|
|
270
228
|
elif config:
|
|
271
|
-
|
|
272
|
-
|
|
229
|
+
asyncio.run(
|
|
230
|
+
self._run_config_section(
|
|
231
|
+
config, arg_type, output_list, script, run_name, logger,
|
|
232
|
+
socketio, run_id=run_id, compiled=compiled, batch_mode=batch_mode, batch_size=batch_size
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
# self._run_config_section(config, arg_type, output_list, script, run_name, logger,
|
|
236
|
+
# socketio, run_id=run_id, compiled=compiled)
|
|
273
237
|
# Run "cleanup" section once
|
|
274
|
-
self._run_actions(script, section_name="cleanup", logger=logger, socketio=socketio,run_id=run_id)
|
|
238
|
+
asyncio.run(self._run_actions(script, section_name="cleanup", logger=logger, socketio=socketio,run_id=run_id))
|
|
275
239
|
# Reset the running flag when done
|
|
276
240
|
# Save results if necessary
|
|
277
241
|
if not script.python_script and return_list:
|
|
242
|
+
# print(output_list)
|
|
243
|
+
|
|
278
244
|
filename = self._save_results(run_name, arg_type, return_list, output_list, logger, output_path)
|
|
279
245
|
self._emit_progress(socketio, 100)
|
|
280
246
|
|
|
@@ -282,7 +248,8 @@ class ScriptRunner:
|
|
|
282
248
|
logger.error(f"Error during script execution: {e.__str__()}")
|
|
283
249
|
error_flag = True
|
|
284
250
|
finally:
|
|
285
|
-
self.lock.
|
|
251
|
+
if self.lock.locked():
|
|
252
|
+
self.lock.release()
|
|
286
253
|
with current_app.app_context():
|
|
287
254
|
run = db.session.get(WorkflowRun, run_id)
|
|
288
255
|
if run is None:
|
|
@@ -294,7 +261,7 @@ class ScriptRunner:
|
|
|
294
261
|
db.session.commit()
|
|
295
262
|
|
|
296
263
|
|
|
297
|
-
def _run_actions(self, script, section_name="", logger=None, socketio=None, run_id=None):
|
|
264
|
+
async def _run_actions(self, script, section_name="", logger=None, socketio=None, run_id=None):
|
|
298
265
|
_func_str = script.python_script or script.compile()
|
|
299
266
|
step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
|
|
300
267
|
if not step_list:
|
|
@@ -316,14 +283,15 @@ class ScriptRunner:
|
|
|
316
283
|
db.session.flush()
|
|
317
284
|
phase_id = phase.id
|
|
318
285
|
|
|
319
|
-
step_outputs = self.exec_steps(script, section_name,
|
|
286
|
+
step_outputs = await self.exec_steps(script, section_name, phase_id=phase_id)
|
|
320
287
|
# Save phase-level output
|
|
321
288
|
phase.outputs = step_outputs
|
|
322
289
|
phase.end_time = datetime.now()
|
|
323
290
|
db.session.commit()
|
|
324
291
|
return step_outputs
|
|
325
292
|
|
|
326
|
-
def _run_config_section(self, config, arg_type, output_list, script, run_name, logger, socketio, run_id,
|
|
293
|
+
async def _run_config_section(self, config, arg_type, output_list, script, run_name, logger, socketio, run_id,
|
|
294
|
+
compiled=True, batch_mode=False, batch_size=1):
|
|
327
295
|
if not compiled:
|
|
328
296
|
for i in config:
|
|
329
297
|
try:
|
|
@@ -334,13 +302,16 @@ class ScriptRunner:
|
|
|
334
302
|
compiled = False
|
|
335
303
|
break
|
|
336
304
|
if compiled:
|
|
337
|
-
|
|
338
|
-
|
|
305
|
+
batch_size = int(batch_size)
|
|
306
|
+
nested_list = [config[i:i + batch_size] for i in range(0, len(config), batch_size)]
|
|
307
|
+
|
|
308
|
+
for i, kwargs_list in enumerate(nested_list):
|
|
309
|
+
# kwargs = dict(kwargs)
|
|
339
310
|
if self.stop_pending_event.is_set():
|
|
340
311
|
logger.info(f'Stopping execution during {run_name}: {i + 1}/{len(config)}')
|
|
341
312
|
break
|
|
342
|
-
logger.info(f'Executing {i + 1} of {len(
|
|
343
|
-
progress = ((i + 1) * 100 / len(
|
|
313
|
+
logger.info(f'Executing {i + 1} of {len(nested_list)} with kwargs = {kwargs_list}')
|
|
314
|
+
progress = ((i + 1) * 100 / len(nested_list)) - 0.1
|
|
344
315
|
self._emit_progress(socketio, progress)
|
|
345
316
|
# fname = f"{run_name}_script"
|
|
346
317
|
# function = self.globals_dict[fname]
|
|
@@ -349,30 +320,33 @@ class ScriptRunner:
|
|
|
349
320
|
run_id=run_id,
|
|
350
321
|
name="main",
|
|
351
322
|
repeat_index=i,
|
|
352
|
-
parameters=
|
|
323
|
+
parameters=kwargs_list,
|
|
353
324
|
start_time=datetime.now()
|
|
354
325
|
)
|
|
355
326
|
db.session.add(phase)
|
|
356
327
|
db.session.flush()
|
|
357
328
|
|
|
358
329
|
phase_id = phase.id
|
|
359
|
-
output = self.exec_steps(script, "script",
|
|
330
|
+
output = await self.exec_steps(script, "script", phase_id, kwargs_list=kwargs_list, )
|
|
331
|
+
# print(output)
|
|
360
332
|
if output:
|
|
361
333
|
# kwargs.update(output)
|
|
362
|
-
|
|
363
|
-
|
|
334
|
+
for output_dict in output:
|
|
335
|
+
output_list.append(output_dict)
|
|
336
|
+
phase.outputs = output
|
|
364
337
|
phase.end_time = datetime.now()
|
|
365
338
|
db.session.commit()
|
|
339
|
+
return output_list
|
|
366
340
|
|
|
367
|
-
def _run_repeat_section(self, repeat_count, arg_types, bo_args, output_list, script, run_name, return_list, compiled,
|
|
368
|
-
logger, socketio, history, output_path, run_id, optimizer=None
|
|
341
|
+
async def _run_repeat_section(self, repeat_count, arg_types, bo_args, output_list, script, run_name, return_list, compiled,
|
|
342
|
+
logger, socketio, history, output_path, run_id, optimizer=None, batch_mode=None,
|
|
343
|
+
batch_size=None, objectives=None):
|
|
369
344
|
if bo_args:
|
|
370
345
|
logger.info('Initializing optimizer...')
|
|
371
346
|
if compiled:
|
|
372
347
|
ax_client = bo_campaign.ax_init_opc(bo_args)
|
|
373
348
|
else:
|
|
374
349
|
if history:
|
|
375
|
-
import pandas as pd
|
|
376
350
|
file_path = os.path.join(output_path, history)
|
|
377
351
|
previous_runs = pd.read_csv(file_path).to_dict(orient='records')
|
|
378
352
|
ax_client = bo_campaign.ax_init_form(bo_args, arg_types, len(previous_runs))
|
|
@@ -385,12 +359,26 @@ class ScriptRunner:
|
|
|
385
359
|
else:
|
|
386
360
|
ax_client = bo_campaign.ax_init_form(bo_args, arg_types)
|
|
387
361
|
elif optimizer and history:
|
|
388
|
-
import pandas as pd
|
|
389
362
|
file_path = os.path.join(output_path, history)
|
|
390
363
|
|
|
391
364
|
previous_runs = pd.read_csv(file_path)
|
|
365
|
+
|
|
366
|
+
expected_cols = list(arg_types.keys()) + list(return_list)
|
|
367
|
+
|
|
368
|
+
actual_cols = previous_runs.columns.tolist()
|
|
369
|
+
|
|
370
|
+
# NOT okay if it misses columns
|
|
371
|
+
if set(expected_cols) - set(actual_cols):
|
|
372
|
+
logger.warning(f"Missing columns from history .csv file. Expecting {expected_cols} but got {actual_cols}")
|
|
373
|
+
raise ValueError("Missing columns from history .csv file.")
|
|
374
|
+
|
|
375
|
+
# okay if there is extra columns
|
|
376
|
+
if set(actual_cols) - set(expected_cols):
|
|
377
|
+
logger.warning(f"Extra columns from history .csv file. Expecting {expected_cols} but got {actual_cols}")
|
|
378
|
+
|
|
392
379
|
optimizer.append_existing_data(previous_runs)
|
|
393
|
-
|
|
380
|
+
|
|
381
|
+
for row in previous_runs.to_dict(orient='records'):
|
|
394
382
|
output_list.append(row)
|
|
395
383
|
|
|
396
384
|
|
|
@@ -416,12 +404,13 @@ class ScriptRunner:
|
|
|
416
404
|
if bo_args:
|
|
417
405
|
try:
|
|
418
406
|
parameters, trial_index = ax_client.get_next_trial()
|
|
419
|
-
logger.info(f'
|
|
407
|
+
logger.info(f'Params value: {parameters}')
|
|
420
408
|
# fname = f"{run_name}_script"
|
|
421
409
|
# function = self.globals_dict[fname]
|
|
422
410
|
phase.parameters = parameters
|
|
423
|
-
|
|
424
|
-
|
|
411
|
+
if not type(parameters) is list:
|
|
412
|
+
parameters = [parameters]
|
|
413
|
+
output = await self.exec_steps(script, "script", phase_id, parameters)
|
|
425
414
|
|
|
426
415
|
_output = {key: value for key, value in output.items() if key in return_list}
|
|
427
416
|
ax_client.complete_trial(trial_index=trial_index, raw_data=_output)
|
|
@@ -432,28 +421,38 @@ class ScriptRunner:
|
|
|
432
421
|
# Optimizer for UI
|
|
433
422
|
elif optimizer:
|
|
434
423
|
try:
|
|
435
|
-
parameters = optimizer.suggest(
|
|
436
|
-
logger.info(f'
|
|
424
|
+
parameters = optimizer.suggest(n=batch_size)
|
|
425
|
+
logger.info(f'Parameters: {parameters}')
|
|
437
426
|
phase.parameters = parameters
|
|
438
|
-
|
|
427
|
+
|
|
428
|
+
output = await self.exec_steps(script, "script", phase_id, kwargs_list=parameters)
|
|
439
429
|
if output:
|
|
440
430
|
optimizer.observe(output)
|
|
441
|
-
|
|
431
|
+
|
|
432
|
+
else:
|
|
433
|
+
logger.info('No output from script')
|
|
434
|
+
|
|
435
|
+
|
|
442
436
|
except Exception as e:
|
|
443
437
|
logger.info(f'Optimization error: {e}')
|
|
444
438
|
break
|
|
445
439
|
else:
|
|
446
440
|
# fname = f"{run_name}_script"
|
|
447
441
|
# function = self.globals_dict[fname]
|
|
448
|
-
output = self.exec_steps(script, "script",
|
|
449
|
-
|
|
442
|
+
output = await self.exec_steps(script, "script", phase_id, batch_size=batch_size)
|
|
450
443
|
if output:
|
|
451
|
-
|
|
444
|
+
# print("output: ", output)
|
|
445
|
+
output_list.extend(output)
|
|
452
446
|
logger.info(f'Output value: {output}')
|
|
453
447
|
phase.outputs = output
|
|
454
448
|
phase.end_time = datetime.now()
|
|
455
449
|
db.session.commit()
|
|
456
|
-
|
|
450
|
+
|
|
451
|
+
early_stop = self._check_early_stop(output, objectives)
|
|
452
|
+
if early_stop:
|
|
453
|
+
logger.info('Early stopping')
|
|
454
|
+
break
|
|
455
|
+
|
|
457
456
|
if bo_args:
|
|
458
457
|
ax_client.save_to_json_file(os.path.join(output_path, f"{run_name}_ax_client.json"))
|
|
459
458
|
logger.info(
|
|
@@ -463,14 +462,14 @@ class ScriptRunner:
|
|
|
463
462
|
|
|
464
463
|
@staticmethod
|
|
465
464
|
def _save_results(run_name, arg_type, return_list, output_list, logger, output_path):
|
|
466
|
-
|
|
467
|
-
|
|
465
|
+
output_columns = list(arg_type.keys()) + list(return_list)
|
|
466
|
+
|
|
468
467
|
filename = run_name + "_" + datetime.now().strftime("%Y-%m-%d %H-%M") + ".csv"
|
|
469
468
|
file_path = os.path.join(output_path, filename)
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
469
|
+
df = pd.DataFrame(output_list)
|
|
470
|
+
df = df.loc[:, [c for c in output_columns if c in df.columns]]
|
|
471
|
+
|
|
472
|
+
df. to_csv(file_path, index=False)
|
|
474
473
|
logger.info(f'Results saved to {file_path}')
|
|
475
474
|
return filename
|
|
476
475
|
|
|
@@ -494,4 +493,298 @@ class ScriptRunner:
|
|
|
494
493
|
"paused": self.paused,
|
|
495
494
|
"stop_pending": self.stop_pending_event.is_set(),
|
|
496
495
|
"stop_current": self.stop_current_event.is_set(),
|
|
497
|
-
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
async def _execute_steps_batched(self, steps: List[Dict], contexts: List[Dict[str, Any]], phase_id, step_index, section_name):
|
|
500
|
+
"""
|
|
501
|
+
Execute a list of steps for multiple samples, batching where appropriate.
|
|
502
|
+
"""
|
|
503
|
+
for step in steps:
|
|
504
|
+
action = step["action"]
|
|
505
|
+
|
|
506
|
+
if action == "if":
|
|
507
|
+
await self._execute_if_batched(step, contexts, phase_id, step_index, section_name)
|
|
508
|
+
elif action == "repeat":
|
|
509
|
+
await self._execute_repeat_batched(step, contexts, phase_id, step_index, section_name)
|
|
510
|
+
elif action == "while":
|
|
511
|
+
await self._execute_while_batched(step, contexts, phase_id, step_index, section_name)
|
|
512
|
+
else:
|
|
513
|
+
# Regular action - check if batch
|
|
514
|
+
if step.get("batch_action", False):
|
|
515
|
+
# Execute once for all samples
|
|
516
|
+
await self._execute_action_once(step, contexts[0], phase_id=phase_id, step_index=step_index, section_name=section_name)
|
|
517
|
+
else:
|
|
518
|
+
# Execute for each sample
|
|
519
|
+
for context in contexts:
|
|
520
|
+
await self._execute_action(step, context, phase_id=phase_id, step_index=step_index, section_name=section_name)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
async def _execute_if_batched(self, step: Dict, contexts: List[Dict[str, Any]], phase_id, step_index, section_name):
|
|
524
|
+
"""Execute if/else block for multiple samples."""
|
|
525
|
+
# Evaluate condition for each sample
|
|
526
|
+
for context in contexts:
|
|
527
|
+
condition = self._evaluate_condition(step["args"]["statement"], context)
|
|
528
|
+
|
|
529
|
+
if condition:
|
|
530
|
+
await self._execute_steps_batched(step["if_block"], [context], phase_id=phase_id, step_index=step_index, section_name=section_name)
|
|
531
|
+
else:
|
|
532
|
+
await self._execute_steps_batched(step["else_block"], [context], phase_id=phase_id, step_index=step_index, section_name=section_name)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
async def _execute_repeat_batched(self, step: Dict, contexts: List[Dict[str, Any]], phase_id, step_index, section_name):
|
|
536
|
+
"""Execute repeat block for multiple samples."""
|
|
537
|
+
times = step["args"].get("statement", 1)
|
|
538
|
+
|
|
539
|
+
for i in range(times):
|
|
540
|
+
# Add repeat index to all contexts
|
|
541
|
+
# for context in contexts:
|
|
542
|
+
# context["repeat_index"] = i
|
|
543
|
+
|
|
544
|
+
await self._execute_steps_batched(step["repeat_block"], contexts, phase_id=phase_id, step_index=step_index, section_name=section_name)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
async def _execute_while_batched(self, step: Dict, contexts: List[Dict[str, Any]], phase_id, step_index, section_name):
|
|
548
|
+
"""Execute while block for multiple samples."""
|
|
549
|
+
max_iterations = step["args"].get("max_iterations", 1000)
|
|
550
|
+
active_contexts = contexts.copy()
|
|
551
|
+
iteration = 0
|
|
552
|
+
|
|
553
|
+
while iteration < max_iterations and active_contexts:
|
|
554
|
+
# Filter contexts that still meet the condition
|
|
555
|
+
still_active = []
|
|
556
|
+
|
|
557
|
+
for context in active_contexts:
|
|
558
|
+
condition = self._evaluate_condition(step["args"]["statement"], context)
|
|
559
|
+
|
|
560
|
+
if condition:
|
|
561
|
+
context["while_index"] = iteration
|
|
562
|
+
still_active.append(context)
|
|
563
|
+
|
|
564
|
+
if not still_active:
|
|
565
|
+
break
|
|
566
|
+
|
|
567
|
+
# Execute for contexts that are still active
|
|
568
|
+
await self._execute_steps_batched(step["while_block"], still_active, phase_id=phase_id, step_index=step_index, section_name=section_name)
|
|
569
|
+
active_contexts = still_active
|
|
570
|
+
iteration += 1
|
|
571
|
+
|
|
572
|
+
if iteration >= max_iterations:
|
|
573
|
+
raise RuntimeError(f"While loop exceeded max iterations ({max_iterations})")
|
|
574
|
+
|
|
575
|
+
async def _execute_action(self, step: Dict, context: Dict[str, Any], phase_id=1, step_index=1, section_name=None):
|
|
576
|
+
"""Execute a single action with parameter substitution."""
|
|
577
|
+
# Substitute parameters in args
|
|
578
|
+
substituted_args = self._substitute_params(step["args"], context)
|
|
579
|
+
|
|
580
|
+
# Get the component and method
|
|
581
|
+
instrument = step.get("instrument", "")
|
|
582
|
+
action = step["action"]
|
|
583
|
+
if instrument and "." in instrument:
|
|
584
|
+
instrument_type, instrument = instrument.split(".")
|
|
585
|
+
else:
|
|
586
|
+
instrument_type = ""
|
|
587
|
+
# Execute the action
|
|
588
|
+
step_db = WorkflowStep(
|
|
589
|
+
phase_id=phase_id,
|
|
590
|
+
step_index=step_index,
|
|
591
|
+
method_name=action,
|
|
592
|
+
start_time=datetime.now(),
|
|
593
|
+
)
|
|
594
|
+
db.session.add(step_db)
|
|
595
|
+
db.session.flush()
|
|
596
|
+
try:
|
|
597
|
+
# print(f"step {section_name}-{step_index}")
|
|
598
|
+
self.socketio.emit('execution', {'section': f"{section_name}-{step_index-1}"})
|
|
599
|
+
if action == "wait":
|
|
600
|
+
duration = float(substituted_args["statement"])
|
|
601
|
+
self.safe_sleep(duration)
|
|
602
|
+
|
|
603
|
+
elif action == "pause":
|
|
604
|
+
msg = substituted_args.get("statement", "")
|
|
605
|
+
pause(msg)
|
|
606
|
+
|
|
607
|
+
elif instrument_type == "deck" and hasattr(deck, instrument):
|
|
608
|
+
component = getattr(deck, instrument)
|
|
609
|
+
if hasattr(component, action):
|
|
610
|
+
method = getattr(component, action)
|
|
611
|
+
|
|
612
|
+
# Execute and handle return value
|
|
613
|
+
if step.get("coroutine", False):
|
|
614
|
+
result = await method(**substituted_args)
|
|
615
|
+
else:
|
|
616
|
+
result = method(**substituted_args)
|
|
617
|
+
|
|
618
|
+
# Store return value if specified
|
|
619
|
+
return_var = step.get("return", "")
|
|
620
|
+
if return_var:
|
|
621
|
+
context[return_var] = result
|
|
622
|
+
|
|
623
|
+
elif instrument_type == "blocks" and instrument in BUILDING_BLOCKS.keys():
|
|
624
|
+
# Inject all block categories
|
|
625
|
+
method_collection = BUILDING_BLOCKS[instrument]
|
|
626
|
+
if action in method_collection.keys():
|
|
627
|
+
method = method_collection[action]["func"]
|
|
628
|
+
|
|
629
|
+
# Execute and handle return value
|
|
630
|
+
# print(step.get("coroutine", False))
|
|
631
|
+
if step.get("coroutine", False):
|
|
632
|
+
result = await method(**substituted_args)
|
|
633
|
+
else:
|
|
634
|
+
result = method(**substituted_args)
|
|
635
|
+
|
|
636
|
+
# Store return value if specified
|
|
637
|
+
return_var = step.get("return", "")
|
|
638
|
+
if return_var:
|
|
639
|
+
context[return_var] = result
|
|
640
|
+
except HumanInterventionRequired as e:
|
|
641
|
+
self.logger.warning(f"Human intervention required: {e}")
|
|
642
|
+
self.socketio.emit('human_intervention', {'message': str(e)})
|
|
643
|
+
# Instead of auto-resume, explicitly stay paused until user action
|
|
644
|
+
# step.run_error = False
|
|
645
|
+
self.toggle_pause()
|
|
646
|
+
|
|
647
|
+
except Exception as e:
|
|
648
|
+
self.logger.error(f"Error during script execution: {e}")
|
|
649
|
+
self.socketio.emit('error', {'message': str(e)})
|
|
650
|
+
|
|
651
|
+
step_db.run_error = True
|
|
652
|
+
self.toggle_pause()
|
|
653
|
+
finally:
|
|
654
|
+
step_db.end_time = datetime.now()
|
|
655
|
+
step_db.output = context
|
|
656
|
+
db.session.commit()
|
|
657
|
+
|
|
658
|
+
self.pause_event.wait()
|
|
659
|
+
|
|
660
|
+
return context
|
|
661
|
+
|
|
662
|
+
async def _execute_action_once(self, step: Dict, context: Dict[str, Any], phase_id, step_index, section_name):
|
|
663
|
+
"""Execute a batch action once (not per sample)."""
|
|
664
|
+
# print(f"Executing batch action: {step['action']}")
|
|
665
|
+
return await self._execute_action(step, context, phase_id=phase_id, step_index=step_index, section_name=section_name)
|
|
666
|
+
|
|
667
|
+
@staticmethod
|
|
668
|
+
def _substitute_params(args: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
|
669
|
+
"""Substitute parameter placeholders like #param_1 with actual values."""
|
|
670
|
+
substituted = {}
|
|
671
|
+
|
|
672
|
+
for key, value in args.items():
|
|
673
|
+
if isinstance(value, str) and value.startswith("#"):
|
|
674
|
+
param_name = value[1:] # Remove '#'
|
|
675
|
+
substituted[key] = context.get(param_name, value)
|
|
676
|
+
else:
|
|
677
|
+
substituted[key] = value
|
|
678
|
+
|
|
679
|
+
return substituted
|
|
680
|
+
|
|
681
|
+
@staticmethod
|
|
682
|
+
def _evaluate_condition(condition_str: str, context: Dict[str, Any]) -> bool:
|
|
683
|
+
"""
|
|
684
|
+
Safely evaluate a condition string with context variables.
|
|
685
|
+
"""
|
|
686
|
+
# Create evaluation context with all variables
|
|
687
|
+
eval_context = {}
|
|
688
|
+
|
|
689
|
+
# Substitute variables in the condition string
|
|
690
|
+
substituted = condition_str
|
|
691
|
+
for key, value in context.items():
|
|
692
|
+
# Replace #variable with actual variable name for eval
|
|
693
|
+
substituted = substituted.replace(f"#{key}", key)
|
|
694
|
+
# Add variable to eval context
|
|
695
|
+
eval_context[key] = value
|
|
696
|
+
|
|
697
|
+
try:
|
|
698
|
+
# Safe evaluation with variables in scope
|
|
699
|
+
result = eval(substituted, {"__builtins__": {}}, eval_context)
|
|
700
|
+
return bool(result)
|
|
701
|
+
except Exception as e:
|
|
702
|
+
raise ValueError(f"Error evaluating condition '{condition_str}': {e}")
|
|
703
|
+
|
|
704
|
+
def _check_early_stop(self, output, objectives):
|
|
705
|
+
for row in output:
|
|
706
|
+
all_met = True
|
|
707
|
+
for obj in objectives:
|
|
708
|
+
name = obj['name']
|
|
709
|
+
minimize = obj.get('minimize', True)
|
|
710
|
+
threshold = obj.get('early_stop', None)
|
|
711
|
+
|
|
712
|
+
if threshold is None:
|
|
713
|
+
all_met = False
|
|
714
|
+
break# Skip if no early stop defined
|
|
715
|
+
|
|
716
|
+
value = row[name]
|
|
717
|
+
if minimize and value > threshold:
|
|
718
|
+
all_met = False
|
|
719
|
+
break
|
|
720
|
+
elif not minimize and value < threshold:
|
|
721
|
+
all_met = False
|
|
722
|
+
break
|
|
723
|
+
|
|
724
|
+
if all_met:
|
|
725
|
+
return True # At least one row meets all early stop thresholds
|
|
726
|
+
|
|
727
|
+
return False # No row met all thresholds
|
|
728
|
+
|
|
729
|
+
async def _execute_variable_batched(self, step: Dict, contexts: List[Dict[str, Any]], phase_id, step_index,
|
|
730
|
+
section_name):
|
|
731
|
+
"""Execute variable assignment for multiple samples."""
|
|
732
|
+
var_name = step["action"] # "vial" in your example
|
|
733
|
+
var_value = step["args"]["statement"]
|
|
734
|
+
arg_type = step["arg_types"]["statement"]
|
|
735
|
+
|
|
736
|
+
for context in contexts:
|
|
737
|
+
# Substitute any variable references in the value
|
|
738
|
+
if isinstance(var_value, str):
|
|
739
|
+
substituted_value = var_value
|
|
740
|
+
|
|
741
|
+
# Replace all variable references (with or without #) with their values
|
|
742
|
+
for key, val in context.items():
|
|
743
|
+
# Handle both #variable and variable (without #)
|
|
744
|
+
substituted_value = substituted_value.replace(f"#{key}", str(val))
|
|
745
|
+
# For expressions like "vial+10", replace variable name directly
|
|
746
|
+
# Use word boundaries to avoid partial matches
|
|
747
|
+
import re
|
|
748
|
+
substituted_value = re.sub(r'\b' + re.escape(key) + r'\b', str(val), substituted_value)
|
|
749
|
+
|
|
750
|
+
# Handle based on type
|
|
751
|
+
if arg_type == "float":
|
|
752
|
+
try:
|
|
753
|
+
# Evaluate as expression (e.g., "10.0+10" becomes 20.0)
|
|
754
|
+
result = eval(substituted_value, {"__builtins__": {}}, {})
|
|
755
|
+
context[var_name] = float(result)
|
|
756
|
+
except:
|
|
757
|
+
# If eval fails, try direct conversion
|
|
758
|
+
context[var_name] = float(substituted_value)
|
|
759
|
+
|
|
760
|
+
elif arg_type == "int":
|
|
761
|
+
try:
|
|
762
|
+
result = eval(substituted_value, {"__builtins__": {}}, {})
|
|
763
|
+
context[var_name] = int(result)
|
|
764
|
+
except:
|
|
765
|
+
context[var_name] = int(substituted_value)
|
|
766
|
+
|
|
767
|
+
elif arg_type == "bool":
|
|
768
|
+
try:
|
|
769
|
+
# Evaluate boolean expressions
|
|
770
|
+
result = eval(substituted_value, {"__builtins__": {}}, {})
|
|
771
|
+
context[var_name] = bool(result)
|
|
772
|
+
except:
|
|
773
|
+
context[var_name] = substituted_value.lower() in ['true', '1', 'yes']
|
|
774
|
+
|
|
775
|
+
else: # "str"
|
|
776
|
+
# For strings, check if it looks like an expression
|
|
777
|
+
if any(char in substituted_value for char in ['+', '-', '*', '/', '>', '<', '=', '(', ')']):
|
|
778
|
+
try:
|
|
779
|
+
# Try to evaluate as expression
|
|
780
|
+
result = eval(substituted_value, {"__builtins__": {}}, {})
|
|
781
|
+
context[var_name] = result
|
|
782
|
+
except:
|
|
783
|
+
# If eval fails, store as string
|
|
784
|
+
context[var_name] = substituted_value
|
|
785
|
+
else:
|
|
786
|
+
context[var_name] = substituted_value
|
|
787
|
+
else:
|
|
788
|
+
# Direct numeric or boolean value
|
|
789
|
+
context[var_name] = var_value
|
|
790
|
+
|