ivoryos 1.3.8__py3-none-any.whl → 1.4.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/optimizer/ax_optimizer.py +44 -25
- ivoryos/optimizer/base_optimizer.py +15 -1
- ivoryos/optimizer/baybe_optimizer.py +24 -17
- ivoryos/optimizer/nimo_optimizer.py +26 -24
- 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 +36 -7
- 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 +22 -5
- ivoryos/utils/nest_script.py +314 -0
- ivoryos/utils/script_runner.py +447 -147
- ivoryos/utils/utils.py +11 -1
- ivoryos/version.py +1 -1
- {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/METADATA +3 -2
- {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/RECORD +34 -33
- {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/WHEEL +0 -0
- {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {ivoryos-1.3.8.dist-info → ivoryos-1.4.0.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
|
|
@@ -110,6 +123,8 @@ class ScriptRunner:
|
|
|
110
123
|
:return: The final result of the function execution
|
|
111
124
|
"""
|
|
112
125
|
_func_str = script.python_script or script.compile()
|
|
126
|
+
_, return_list = script.config_return()
|
|
127
|
+
|
|
113
128
|
step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
|
|
114
129
|
global deck
|
|
115
130
|
# global deck, registered_workflows
|
|
@@ -129,112 +144,54 @@ class ScriptRunner:
|
|
|
129
144
|
# exec_globals = {"deck": deck, "time": time, "registered_workflows":registered_workflows} # Add required global objects
|
|
130
145
|
exec_globals.update(temp_connections)
|
|
131
146
|
|
|
132
|
-
# Inject all block categories
|
|
133
|
-
for category, data in BUILDING_BLOCKS.items():
|
|
134
|
-
for method_name, method in data.items():
|
|
135
|
-
exec_globals[method_name] = method["func"]
|
|
136
|
-
|
|
137
147
|
exec_locals = {} # Local execution scope
|
|
138
148
|
|
|
139
149
|
# Define function arguments manually in exec_locals
|
|
140
|
-
exec_locals.update(kwargs)
|
|
150
|
+
# exec_locals.update(kwargs)
|
|
141
151
|
index = 0
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
method_name=method_name,
|
|
170
|
-
start_time=datetime.now(),
|
|
171
|
-
)
|
|
172
|
-
db.session.add(step)
|
|
173
|
-
db.session.flush()
|
|
174
|
-
|
|
175
|
-
logger.info(f"Executing: {line}")
|
|
176
|
-
socketio.emit('execution', {'section': f"{section_name}-{index}"})
|
|
177
|
-
|
|
178
|
-
# if line.startswith("registered_workflows"):
|
|
179
|
-
# line = line.replace("registered_workflows.", "")
|
|
180
|
-
try:
|
|
181
|
-
if line.startswith("time.sleep("): # add safe sleep for time.sleep lines
|
|
182
|
-
duration_str = line.strip()[len("time.sleep("):-1]
|
|
183
|
-
duration = float(duration_str)
|
|
184
|
-
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)
|
|
185
179
|
else:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
exec(async_code, exec_globals, exec_locals)
|
|
192
|
-
func = exec_locals.get("__async_exec_wrapper") or exec_globals.get("__async_exec_wrapper")
|
|
193
|
-
# Capture the return value from asyncio.run
|
|
194
|
-
result_locals = asyncio.run(func())
|
|
195
|
-
|
|
196
|
-
# Update exec_locals with the returned locals
|
|
197
|
-
exec_locals.update(result_locals)
|
|
198
|
-
|
|
199
|
-
|
|
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()
|
|
200
185
|
else:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
# return locals_dict
|
|
205
|
-
# exec(line, exec_globals, exec_locals)
|
|
206
|
-
# step.run_error = False
|
|
207
|
-
|
|
208
|
-
except HumanInterventionRequired as e:
|
|
209
|
-
logger.warning(f"Human intervention required: {e}")
|
|
210
|
-
socketio.emit('human_intervention', {'message': str(e)})
|
|
211
|
-
# Instead of auto-resume, explicitly stay paused until user action
|
|
212
|
-
# step.run_error = False
|
|
213
|
-
self.toggle_pause()
|
|
214
|
-
|
|
215
|
-
except Exception as e:
|
|
216
|
-
logger.error(f"Error during script execution: {e}")
|
|
217
|
-
socketio.emit('error', {'message': str(e)})
|
|
218
|
-
|
|
219
|
-
step.run_error = True
|
|
220
|
-
self.toggle_pause()
|
|
221
|
-
exec_locals.pop("__async_exec_wrapper", None)
|
|
222
|
-
step.end_time = datetime.now()
|
|
223
|
-
step.output = exec_locals
|
|
224
|
-
db.session.commit()
|
|
225
|
-
|
|
226
|
-
self.pause_event.wait()
|
|
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
|
-
# _func_str = script.compile()
|
|
230
|
-
# step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
|
|
231
|
-
if not step.run_error or not self.retry:
|
|
232
|
-
index += 1
|
|
233
|
-
|
|
234
|
-
return exec_locals # Return the 'results' variable
|
|
190
|
+
return results # Return the 'results' variable
|
|
235
191
|
|
|
236
192
|
def _run_with_stop_check(self, script: Script, repeat_count: int, run_name: str, logger, socketio, config, bo_args,
|
|
237
|
-
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):
|
|
238
195
|
time.sleep(1)
|
|
239
196
|
# _func_str = script.compile()
|
|
240
197
|
# step_list_dict: dict = script.convert_to_lines(_func_str)
|
|
@@ -250,27 +207,40 @@ class ScriptRunner:
|
|
|
250
207
|
db.session.add(run)
|
|
251
208
|
db.session.flush()
|
|
252
209
|
run_id = run.id # Save the ID
|
|
253
|
-
|
|
210
|
+
db.session.commit()
|
|
254
211
|
|
|
212
|
+
try:
|
|
213
|
+
# if True:
|
|
255
214
|
global_config.runner_status = {"id":run_id, "type": "workflow"}
|
|
256
215
|
# Run "prep" section once
|
|
257
|
-
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))
|
|
258
217
|
output_list = []
|
|
259
218
|
_, arg_type = script.config("script")
|
|
260
219
|
_, return_list = script.config_return()
|
|
261
220
|
# Run "script" section multiple times
|
|
262
221
|
if repeat_count:
|
|
263
|
-
|
|
222
|
+
asyncio.run(
|
|
223
|
+
self._run_repeat_section(repeat_count, arg_type, bo_args, output_list, script,
|
|
264
224
|
run_name, return_list, compiled, logger, socketio,
|
|
265
|
-
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
|
+
)
|
|
266
228
|
elif config:
|
|
267
|
-
|
|
268
|
-
|
|
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)
|
|
269
237
|
# Run "cleanup" section once
|
|
270
|
-
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))
|
|
271
239
|
# Reset the running flag when done
|
|
272
240
|
# Save results if necessary
|
|
273
241
|
if not script.python_script and return_list:
|
|
242
|
+
# print(output_list)
|
|
243
|
+
|
|
274
244
|
filename = self._save_results(run_name, arg_type, return_list, output_list, logger, output_path)
|
|
275
245
|
self._emit_progress(socketio, 100)
|
|
276
246
|
|
|
@@ -278,16 +248,20 @@ class ScriptRunner:
|
|
|
278
248
|
logger.error(f"Error during script execution: {e.__str__()}")
|
|
279
249
|
error_flag = True
|
|
280
250
|
finally:
|
|
281
|
-
self.lock.
|
|
251
|
+
if self.lock.locked():
|
|
252
|
+
self.lock.release()
|
|
282
253
|
with current_app.app_context():
|
|
283
254
|
run = db.session.get(WorkflowRun, run_id)
|
|
284
|
-
run
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
255
|
+
if run is None:
|
|
256
|
+
logger.info("Error: Run not found in database.")
|
|
257
|
+
else:
|
|
258
|
+
run.end_time = datetime.now()
|
|
259
|
+
run.data_path = filename
|
|
260
|
+
run.run_error = error_flag
|
|
261
|
+
db.session.commit()
|
|
288
262
|
|
|
289
263
|
|
|
290
|
-
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):
|
|
291
265
|
_func_str = script.python_script or script.compile()
|
|
292
266
|
step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
|
|
293
267
|
if not step_list:
|
|
@@ -309,14 +283,15 @@ class ScriptRunner:
|
|
|
309
283
|
db.session.flush()
|
|
310
284
|
phase_id = phase.id
|
|
311
285
|
|
|
312
|
-
step_outputs = self.exec_steps(script, section_name,
|
|
286
|
+
step_outputs = await self.exec_steps(script, section_name, phase_id=phase_id)
|
|
313
287
|
# Save phase-level output
|
|
314
288
|
phase.outputs = step_outputs
|
|
315
289
|
phase.end_time = datetime.now()
|
|
316
290
|
db.session.commit()
|
|
317
291
|
return step_outputs
|
|
318
292
|
|
|
319
|
-
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):
|
|
320
295
|
if not compiled:
|
|
321
296
|
for i in config:
|
|
322
297
|
try:
|
|
@@ -327,13 +302,16 @@ class ScriptRunner:
|
|
|
327
302
|
compiled = False
|
|
328
303
|
break
|
|
329
304
|
if compiled:
|
|
330
|
-
|
|
331
|
-
|
|
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)
|
|
332
310
|
if self.stop_pending_event.is_set():
|
|
333
311
|
logger.info(f'Stopping execution during {run_name}: {i + 1}/{len(config)}')
|
|
334
312
|
break
|
|
335
|
-
logger.info(f'Executing {i + 1} of {len(
|
|
336
|
-
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
|
|
337
315
|
self._emit_progress(socketio, progress)
|
|
338
316
|
# fname = f"{run_name}_script"
|
|
339
317
|
# function = self.globals_dict[fname]
|
|
@@ -342,30 +320,33 @@ class ScriptRunner:
|
|
|
342
320
|
run_id=run_id,
|
|
343
321
|
name="main",
|
|
344
322
|
repeat_index=i,
|
|
345
|
-
parameters=
|
|
323
|
+
parameters=kwargs_list,
|
|
346
324
|
start_time=datetime.now()
|
|
347
325
|
)
|
|
348
326
|
db.session.add(phase)
|
|
349
327
|
db.session.flush()
|
|
350
328
|
|
|
351
329
|
phase_id = phase.id
|
|
352
|
-
output = self.exec_steps(script, "script",
|
|
330
|
+
output = await self.exec_steps(script, "script", phase_id, kwargs_list=kwargs_list, )
|
|
331
|
+
# print(output)
|
|
353
332
|
if output:
|
|
354
333
|
# kwargs.update(output)
|
|
355
|
-
|
|
356
|
-
|
|
334
|
+
for output_dict in output:
|
|
335
|
+
output_list.append(output_dict)
|
|
336
|
+
phase.outputs = output
|
|
357
337
|
phase.end_time = datetime.now()
|
|
358
338
|
db.session.commit()
|
|
339
|
+
return output_list
|
|
359
340
|
|
|
360
|
-
def _run_repeat_section(self, repeat_count, arg_types, bo_args, output_list, script, run_name, return_list, compiled,
|
|
361
|
-
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):
|
|
362
344
|
if bo_args:
|
|
363
345
|
logger.info('Initializing optimizer...')
|
|
364
346
|
if compiled:
|
|
365
347
|
ax_client = bo_campaign.ax_init_opc(bo_args)
|
|
366
348
|
else:
|
|
367
349
|
if history:
|
|
368
|
-
import pandas as pd
|
|
369
350
|
file_path = os.path.join(output_path, history)
|
|
370
351
|
previous_runs = pd.read_csv(file_path).to_dict(orient='records')
|
|
371
352
|
ax_client = bo_campaign.ax_init_form(bo_args, arg_types, len(previous_runs))
|
|
@@ -378,12 +359,26 @@ class ScriptRunner:
|
|
|
378
359
|
else:
|
|
379
360
|
ax_client = bo_campaign.ax_init_form(bo_args, arg_types)
|
|
380
361
|
elif optimizer and history:
|
|
381
|
-
import pandas as pd
|
|
382
362
|
file_path = os.path.join(output_path, history)
|
|
383
363
|
|
|
384
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
|
+
|
|
385
379
|
optimizer.append_existing_data(previous_runs)
|
|
386
|
-
|
|
380
|
+
|
|
381
|
+
for row in previous_runs.to_dict(orient='records'):
|
|
387
382
|
output_list.append(row)
|
|
388
383
|
|
|
389
384
|
|
|
@@ -409,12 +404,13 @@ class ScriptRunner:
|
|
|
409
404
|
if bo_args:
|
|
410
405
|
try:
|
|
411
406
|
parameters, trial_index = ax_client.get_next_trial()
|
|
412
|
-
logger.info(f'
|
|
407
|
+
logger.info(f'Params value: {parameters}')
|
|
413
408
|
# fname = f"{run_name}_script"
|
|
414
409
|
# function = self.globals_dict[fname]
|
|
415
410
|
phase.parameters = parameters
|
|
416
|
-
|
|
417
|
-
|
|
411
|
+
if not type(parameters) is list:
|
|
412
|
+
parameters = [parameters]
|
|
413
|
+
output = await self.exec_steps(script, "script", phase_id, parameters)
|
|
418
414
|
|
|
419
415
|
_output = {key: value for key, value in output.items() if key in return_list}
|
|
420
416
|
ax_client.complete_trial(trial_index=trial_index, raw_data=_output)
|
|
@@ -422,30 +418,41 @@ class ScriptRunner:
|
|
|
422
418
|
except Exception as e:
|
|
423
419
|
logger.info(f'Optimization error: {e}')
|
|
424
420
|
break
|
|
421
|
+
# Optimizer for UI
|
|
425
422
|
elif optimizer:
|
|
426
423
|
try:
|
|
427
|
-
parameters = optimizer.suggest(
|
|
428
|
-
logger.info(f'
|
|
424
|
+
parameters = optimizer.suggest(n=batch_size)
|
|
425
|
+
logger.info(f'Parameters: {parameters}')
|
|
429
426
|
phase.parameters = parameters
|
|
430
|
-
|
|
427
|
+
|
|
428
|
+
output = await self.exec_steps(script, "script", phase_id, kwargs_list=parameters)
|
|
431
429
|
if output:
|
|
432
430
|
optimizer.observe(output)
|
|
433
|
-
|
|
431
|
+
|
|
432
|
+
else:
|
|
433
|
+
logger.info('No output from script')
|
|
434
|
+
|
|
435
|
+
|
|
434
436
|
except Exception as e:
|
|
435
437
|
logger.info(f'Optimization error: {e}')
|
|
436
438
|
break
|
|
437
439
|
else:
|
|
438
440
|
# fname = f"{run_name}_script"
|
|
439
441
|
# function = self.globals_dict[fname]
|
|
440
|
-
output = self.exec_steps(script, "script",
|
|
441
|
-
|
|
442
|
+
output = await self.exec_steps(script, "script", phase_id, batch_size=batch_size)
|
|
442
443
|
if output:
|
|
443
|
-
|
|
444
|
+
# print("output: ", output)
|
|
445
|
+
output_list.extend(output)
|
|
444
446
|
logger.info(f'Output value: {output}')
|
|
445
447
|
phase.outputs = output
|
|
446
448
|
phase.end_time = datetime.now()
|
|
447
449
|
db.session.commit()
|
|
448
|
-
|
|
450
|
+
|
|
451
|
+
early_stop = self._check_early_stop(output, objectives)
|
|
452
|
+
if early_stop:
|
|
453
|
+
logger.info('Early stopping')
|
|
454
|
+
break
|
|
455
|
+
|
|
449
456
|
if bo_args:
|
|
450
457
|
ax_client.save_to_json_file(os.path.join(output_path, f"{run_name}_ax_client.json"))
|
|
451
458
|
logger.info(
|
|
@@ -455,14 +462,14 @@ class ScriptRunner:
|
|
|
455
462
|
|
|
456
463
|
@staticmethod
|
|
457
464
|
def _save_results(run_name, arg_type, return_list, output_list, logger, output_path):
|
|
458
|
-
|
|
459
|
-
|
|
465
|
+
output_columns = list(arg_type.keys()) + list(return_list)
|
|
466
|
+
|
|
460
467
|
filename = run_name + "_" + datetime.now().strftime("%Y-%m-%d %H-%M") + ".csv"
|
|
461
468
|
file_path = os.path.join(output_path, filename)
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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)
|
|
466
473
|
logger.info(f'Results saved to {file_path}')
|
|
467
474
|
return filename
|
|
468
475
|
|
|
@@ -486,4 +493,297 @@ class ScriptRunner:
|
|
|
486
493
|
"paused": self.paused,
|
|
487
494
|
"stop_pending": self.stop_pending_event.is_set(),
|
|
488
495
|
"stop_current": self.stop_current_event.is_set(),
|
|
489
|
-
}
|
|
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("times", 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
|
+
self.socketio.emit('execution', {'section': f"{section_name}-{step_index}"})
|
|
598
|
+
if action == "wait":
|
|
599
|
+
duration = float(substituted_args["statement"])
|
|
600
|
+
self.safe_sleep(duration)
|
|
601
|
+
|
|
602
|
+
elif action == "pause":
|
|
603
|
+
msg = substituted_args.get("statement", "")
|
|
604
|
+
pause(msg)
|
|
605
|
+
|
|
606
|
+
elif instrument_type == "deck" and hasattr(deck, instrument):
|
|
607
|
+
component = getattr(deck, instrument)
|
|
608
|
+
if hasattr(component, action):
|
|
609
|
+
method = getattr(component, action)
|
|
610
|
+
|
|
611
|
+
# Execute and handle return value
|
|
612
|
+
if step.get("coroutine", False):
|
|
613
|
+
result = await method(**substituted_args)
|
|
614
|
+
else:
|
|
615
|
+
result = method(**substituted_args)
|
|
616
|
+
|
|
617
|
+
# Store return value if specified
|
|
618
|
+
return_var = step.get("return", "")
|
|
619
|
+
if return_var:
|
|
620
|
+
context[return_var] = result
|
|
621
|
+
|
|
622
|
+
elif instrument_type == "blocks" and instrument in BUILDING_BLOCKS.keys():
|
|
623
|
+
# Inject all block categories
|
|
624
|
+
method_collection = BUILDING_BLOCKS[instrument]
|
|
625
|
+
if action in method_collection.keys():
|
|
626
|
+
method = method_collection[action]["func"]
|
|
627
|
+
|
|
628
|
+
# Execute and handle return value
|
|
629
|
+
# print(step.get("coroutine", False))
|
|
630
|
+
if step.get("coroutine", False):
|
|
631
|
+
result = await method(**substituted_args)
|
|
632
|
+
else:
|
|
633
|
+
result = method(**substituted_args)
|
|
634
|
+
|
|
635
|
+
# Store return value if specified
|
|
636
|
+
return_var = step.get("return", "")
|
|
637
|
+
if return_var:
|
|
638
|
+
context[return_var] = result
|
|
639
|
+
except HumanInterventionRequired as e:
|
|
640
|
+
self.logger.warning(f"Human intervention required: {e}")
|
|
641
|
+
self.socketio.emit('human_intervention', {'message': str(e)})
|
|
642
|
+
# Instead of auto-resume, explicitly stay paused until user action
|
|
643
|
+
# step.run_error = False
|
|
644
|
+
self.toggle_pause()
|
|
645
|
+
|
|
646
|
+
except Exception as e:
|
|
647
|
+
self.logger.error(f"Error during script execution: {e}")
|
|
648
|
+
self.socketio.emit('error', {'message': str(e)})
|
|
649
|
+
|
|
650
|
+
step_db.run_error = True
|
|
651
|
+
self.toggle_pause()
|
|
652
|
+
finally:
|
|
653
|
+
step_db.end_time = datetime.now()
|
|
654
|
+
step_db.output = context
|
|
655
|
+
db.session.commit()
|
|
656
|
+
|
|
657
|
+
self.pause_event.wait()
|
|
658
|
+
|
|
659
|
+
return context
|
|
660
|
+
|
|
661
|
+
async def _execute_action_once(self, step: Dict, context: Dict[str, Any], phase_id, step_index, section_name):
|
|
662
|
+
"""Execute a batch action once (not per sample)."""
|
|
663
|
+
# print(f"Executing batch action: {step['action']}")
|
|
664
|
+
return await self._execute_action(step, context, phase_id=phase_id, step_index=step_index, section_name=section_name)
|
|
665
|
+
|
|
666
|
+
@staticmethod
|
|
667
|
+
def _substitute_params(args: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
|
668
|
+
"""Substitute parameter placeholders like #param_1 with actual values."""
|
|
669
|
+
substituted = {}
|
|
670
|
+
|
|
671
|
+
for key, value in args.items():
|
|
672
|
+
if isinstance(value, str) and value.startswith("#"):
|
|
673
|
+
param_name = value[1:] # Remove '#'
|
|
674
|
+
substituted[key] = context.get(param_name, value)
|
|
675
|
+
else:
|
|
676
|
+
substituted[key] = value
|
|
677
|
+
|
|
678
|
+
return substituted
|
|
679
|
+
|
|
680
|
+
@staticmethod
|
|
681
|
+
def _evaluate_condition(condition_str: str, context: Dict[str, Any]) -> bool:
|
|
682
|
+
"""
|
|
683
|
+
Safely evaluate a condition string with context variables.
|
|
684
|
+
"""
|
|
685
|
+
# Create evaluation context with all variables
|
|
686
|
+
eval_context = {}
|
|
687
|
+
|
|
688
|
+
# Substitute variables in the condition string
|
|
689
|
+
substituted = condition_str
|
|
690
|
+
for key, value in context.items():
|
|
691
|
+
# Replace #variable with actual variable name for eval
|
|
692
|
+
substituted = substituted.replace(f"#{key}", key)
|
|
693
|
+
# Add variable to eval context
|
|
694
|
+
eval_context[key] = value
|
|
695
|
+
|
|
696
|
+
try:
|
|
697
|
+
# Safe evaluation with variables in scope
|
|
698
|
+
result = eval(substituted, {"__builtins__": {}}, eval_context)
|
|
699
|
+
return bool(result)
|
|
700
|
+
except Exception as e:
|
|
701
|
+
raise ValueError(f"Error evaluating condition '{condition_str}': {e}")
|
|
702
|
+
|
|
703
|
+
def _check_early_stop(self, output, objectives):
|
|
704
|
+
for row in output:
|
|
705
|
+
all_met = True
|
|
706
|
+
for obj in objectives:
|
|
707
|
+
name = obj['name']
|
|
708
|
+
minimize = obj.get('minimize', True)
|
|
709
|
+
threshold = obj.get('early_stop', None)
|
|
710
|
+
|
|
711
|
+
if threshold is None:
|
|
712
|
+
all_met = False
|
|
713
|
+
break# Skip if no early stop defined
|
|
714
|
+
|
|
715
|
+
value = row[name]
|
|
716
|
+
if minimize and value > threshold:
|
|
717
|
+
all_met = False
|
|
718
|
+
break
|
|
719
|
+
elif not minimize and value < threshold:
|
|
720
|
+
all_met = False
|
|
721
|
+
break
|
|
722
|
+
|
|
723
|
+
if all_met:
|
|
724
|
+
return True # At least one row meets all early stop thresholds
|
|
725
|
+
|
|
726
|
+
return False # No row met all thresholds
|
|
727
|
+
|
|
728
|
+
async def _execute_variable_batched(self, step: Dict, contexts: List[Dict[str, Any]], phase_id, step_index,
|
|
729
|
+
section_name):
|
|
730
|
+
"""Execute variable assignment for multiple samples."""
|
|
731
|
+
var_name = step["action"] # "vial" in your example
|
|
732
|
+
var_value = step["args"]["statement"]
|
|
733
|
+
arg_type = step["arg_types"]["statement"]
|
|
734
|
+
|
|
735
|
+
for context in contexts:
|
|
736
|
+
# Substitute any variable references in the value
|
|
737
|
+
if isinstance(var_value, str):
|
|
738
|
+
substituted_value = var_value
|
|
739
|
+
|
|
740
|
+
# Replace all variable references (with or without #) with their values
|
|
741
|
+
for key, val in context.items():
|
|
742
|
+
# Handle both #variable and variable (without #)
|
|
743
|
+
substituted_value = substituted_value.replace(f"#{key}", str(val))
|
|
744
|
+
# For expressions like "vial+10", replace variable name directly
|
|
745
|
+
# Use word boundaries to avoid partial matches
|
|
746
|
+
import re
|
|
747
|
+
substituted_value = re.sub(r'\b' + re.escape(key) + r'\b', str(val), substituted_value)
|
|
748
|
+
|
|
749
|
+
# Handle based on type
|
|
750
|
+
if arg_type == "float":
|
|
751
|
+
try:
|
|
752
|
+
# Evaluate as expression (e.g., "10.0+10" becomes 20.0)
|
|
753
|
+
result = eval(substituted_value, {"__builtins__": {}}, {})
|
|
754
|
+
context[var_name] = float(result)
|
|
755
|
+
except:
|
|
756
|
+
# If eval fails, try direct conversion
|
|
757
|
+
context[var_name] = float(substituted_value)
|
|
758
|
+
|
|
759
|
+
elif arg_type == "int":
|
|
760
|
+
try:
|
|
761
|
+
result = eval(substituted_value, {"__builtins__": {}}, {})
|
|
762
|
+
context[var_name] = int(result)
|
|
763
|
+
except:
|
|
764
|
+
context[var_name] = int(substituted_value)
|
|
765
|
+
|
|
766
|
+
elif arg_type == "bool":
|
|
767
|
+
try:
|
|
768
|
+
# Evaluate boolean expressions
|
|
769
|
+
result = eval(substituted_value, {"__builtins__": {}}, {})
|
|
770
|
+
context[var_name] = bool(result)
|
|
771
|
+
except:
|
|
772
|
+
context[var_name] = substituted_value.lower() in ['true', '1', 'yes']
|
|
773
|
+
|
|
774
|
+
else: # "str"
|
|
775
|
+
# For strings, check if it looks like an expression
|
|
776
|
+
if any(char in substituted_value for char in ['+', '-', '*', '/', '>', '<', '=', '(', ')']):
|
|
777
|
+
try:
|
|
778
|
+
# Try to evaluate as expression
|
|
779
|
+
result = eval(substituted_value, {"__builtins__": {}}, {})
|
|
780
|
+
context[var_name] = result
|
|
781
|
+
except:
|
|
782
|
+
# If eval fails, store as string
|
|
783
|
+
context[var_name] = substituted_value
|
|
784
|
+
else:
|
|
785
|
+
context[var_name] = substituted_value
|
|
786
|
+
else:
|
|
787
|
+
# Direct numeric or boolean value
|
|
788
|
+
context[var_name] = var_value
|
|
789
|
+
|