ivoryos 1.0.9__py3-none-any.whl → 1.4.4__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.
- docs/source/conf.py +84 -0
- ivoryos/__init__.py +17 -207
- ivoryos/app.py +154 -0
- ivoryos/config.py +1 -0
- ivoryos/optimizer/ax_optimizer.py +191 -0
- ivoryos/optimizer/base_optimizer.py +84 -0
- ivoryos/optimizer/baybe_optimizer.py +193 -0
- ivoryos/optimizer/nimo_optimizer.py +173 -0
- ivoryos/optimizer/registry.py +11 -0
- ivoryos/routes/auth/auth.py +43 -14
- ivoryos/routes/auth/templates/change_password.html +32 -0
- ivoryos/routes/control/control.py +101 -366
- ivoryos/routes/control/control_file.py +33 -0
- ivoryos/routes/control/control_new_device.py +152 -0
- ivoryos/routes/control/templates/controllers.html +193 -0
- ivoryos/routes/control/templates/controllers_new.html +112 -0
- ivoryos/routes/control/utils.py +40 -0
- ivoryos/routes/data/data.py +197 -0
- ivoryos/routes/data/templates/components/step_card.html +78 -0
- ivoryos/routes/{database/templates/database → data/templates}/workflow_database.html +14 -8
- ivoryos/routes/data/templates/workflow_view.html +360 -0
- ivoryos/routes/design/__init__.py +4 -0
- ivoryos/routes/design/design.py +348 -657
- ivoryos/routes/design/design_file.py +68 -0
- ivoryos/routes/design/design_step.py +171 -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 +39 -0
- ivoryos/routes/design/templates/components/deck_selector.html +10 -0
- ivoryos/routes/design/templates/components/edit_action_form.html +53 -0
- ivoryos/routes/design/templates/components/info_modal.html +318 -0
- ivoryos/routes/design/templates/components/instruments_panel.html +88 -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 +56 -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 +44 -0
- ivoryos/routes/execute/__init__.py +0 -0
- ivoryos/routes/execute/execute.py +377 -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 +56 -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 +60 -0
- ivoryos/routes/execute/templates/components/tab_bayesian.html +520 -0
- ivoryos/routes/execute/templates/components/tab_configuration.html +383 -0
- ivoryos/routes/execute/templates/components/tab_repeat.html +18 -0
- ivoryos/routes/execute/templates/experiment_run.html +30 -0
- ivoryos/routes/library/__init__.py +0 -0
- ivoryos/routes/library/library.py +157 -0
- ivoryos/routes/{database/templates/database/scripts_database.html → library/templates/library.html} +32 -23
- ivoryos/routes/main/main.py +31 -3
- ivoryos/routes/main/templates/{main/home.html → home.html} +4 -4
- ivoryos/server.py +180 -0
- ivoryos/socket_handlers.py +52 -0
- ivoryos/static/ivoryos_logo.png +0 -0
- ivoryos/static/js/action_handlers.js +384 -0
- ivoryos/static/js/db_delete.js +23 -0
- ivoryos/static/js/script_metadata.js +39 -0
- ivoryos/static/js/socket_handler.js +40 -5
- ivoryos/static/js/sortable_design.js +107 -56
- ivoryos/static/js/ui_state.js +114 -0
- ivoryos/templates/base.html +67 -8
- ivoryos/utils/bo_campaign.py +180 -3
- ivoryos/utils/client_proxy.py +267 -36
- ivoryos/utils/db_models.py +300 -65
- ivoryos/utils/decorators.py +34 -0
- ivoryos/utils/form.py +63 -29
- ivoryos/utils/global_config.py +34 -1
- ivoryos/utils/nest_script.py +314 -0
- ivoryos/utils/py_to_json.py +295 -0
- ivoryos/utils/script_runner.py +599 -165
- ivoryos/utils/serilize.py +201 -0
- ivoryos/utils/task_runner.py +71 -21
- ivoryos/utils/utils.py +50 -6
- ivoryos/version.py +1 -1
- ivoryos-1.4.4.dist-info/METADATA +263 -0
- ivoryos-1.4.4.dist-info/RECORD +119 -0
- {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +1 -1
- {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/top_level.txt +1 -0
- tests/unit/test_type_conversion.py +42 -0
- tests/unit/test_util.py +3 -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/database/templates/database/workflow_view.html +0 -130
- 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/METADATA +0 -218
- 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.4.4.dist-info/licenses}/LICENSE +0 -0
ivoryos/utils/db_models.py
CHANGED
|
@@ -27,6 +27,9 @@ class User(db.Model, UserMixin):
|
|
|
27
27
|
# email = db.Column(db.String)
|
|
28
28
|
hashPassword = db.Column(db.String(255))
|
|
29
29
|
|
|
30
|
+
# New columns for logo customization
|
|
31
|
+
settings = db.Column(JSONType, nullable=True)
|
|
32
|
+
|
|
30
33
|
# password = db.Column()
|
|
31
34
|
def __init__(self, username, password):
|
|
32
35
|
# self.id = id
|
|
@@ -122,7 +125,7 @@ class Script(db.Model):
|
|
|
122
125
|
pass
|
|
123
126
|
raise TypeError(f"Input type error: cannot convert '{args}' to {arg_type}.")
|
|
124
127
|
|
|
125
|
-
def update_by_uuid(self, uuid, args, output):
|
|
128
|
+
def update_by_uuid(self, uuid, args, output, batch_action=False):
|
|
126
129
|
action = self.find_by_uuid(uuid)
|
|
127
130
|
if not action:
|
|
128
131
|
return
|
|
@@ -134,6 +137,7 @@ class Script(db.Model):
|
|
|
134
137
|
pass
|
|
135
138
|
action['args'] = args
|
|
136
139
|
action['return'] = output
|
|
140
|
+
action['batch_action'] = batch_action
|
|
137
141
|
|
|
138
142
|
@staticmethod
|
|
139
143
|
def eval_list(args, arg_types):
|
|
@@ -265,11 +269,20 @@ class Script(db.Model):
|
|
|
265
269
|
output_variables: Dict[str, str] = self.get_variables()
|
|
266
270
|
# print(output_variables)
|
|
267
271
|
for key, value in kwargs.items():
|
|
268
|
-
if
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
272
|
+
if isinstance(value, str):
|
|
273
|
+
if value in output_variables:
|
|
274
|
+
var_type = output_variables[value]
|
|
275
|
+
kwargs[key] = f"#{value}"
|
|
276
|
+
elif value.startswith("#"):
|
|
277
|
+
kwargs[key] = f"#{self.validate_function_name(value[1:])}"
|
|
278
|
+
else:
|
|
279
|
+
# attempt to convert to numerical or bool value for args with no type hint
|
|
280
|
+
try:
|
|
281
|
+
converted = ast.literal_eval(value)
|
|
282
|
+
if isinstance(converted, (int, float, bool)):
|
|
283
|
+
kwargs[key] = converted
|
|
284
|
+
except (ValueError, SyntaxError):
|
|
285
|
+
pass
|
|
273
286
|
return kwargs
|
|
274
287
|
|
|
275
288
|
def add_logic_action(self, logic_type: str, statement, insert_position=None):
|
|
@@ -309,6 +322,12 @@ class Script(db.Model):
|
|
|
309
322
|
{"id": current_len + 2, "instrument": 'repeat', "action": 'endrepeat',
|
|
310
323
|
"args": {}, "return": '', "uuid": uid},
|
|
311
324
|
],
|
|
325
|
+
"pause":
|
|
326
|
+
[
|
|
327
|
+
{"id": current_len + 1, "instrument": 'pause', "action": "pause",
|
|
328
|
+
"args": {"statement": 1 if statement == '' else statement}, "return": '', "uuid": uid,
|
|
329
|
+
"arg_types": {"statement": "str"}}
|
|
330
|
+
],
|
|
312
331
|
}
|
|
313
332
|
action_list = logic_dict[logic_type]
|
|
314
333
|
self.currently_editing_script.extend(action_list)
|
|
@@ -355,28 +374,32 @@ class Script(db.Model):
|
|
|
355
374
|
:return: list of variable that require input
|
|
356
375
|
"""
|
|
357
376
|
configure = []
|
|
377
|
+
variables = self.get_variables()
|
|
378
|
+
# print(variables)
|
|
358
379
|
config_type_dict = {}
|
|
359
380
|
for action in self.script_dict[stype]:
|
|
360
381
|
args = action['args']
|
|
361
382
|
if args is not None:
|
|
362
383
|
if type(args) is not dict:
|
|
363
|
-
if type(args) is str and args.startswith("#")
|
|
364
|
-
|
|
365
|
-
|
|
384
|
+
if type(args) is str and args.startswith("#"):
|
|
385
|
+
key = args[1:]
|
|
386
|
+
if key not in (*variables, *configure):
|
|
387
|
+
configure.append(key)
|
|
388
|
+
config_type_dict[key] = action['arg_types']
|
|
366
389
|
|
|
367
390
|
else:
|
|
368
391
|
for arg in args:
|
|
369
|
-
if type(args[arg]) is str
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
392
|
+
if type(args[arg]) is str and args[arg].startswith("#"):
|
|
393
|
+
key = args[arg][1:]
|
|
394
|
+
if key not in (*variables, *configure):
|
|
395
|
+
configure.append(key)
|
|
396
|
+
if arg in action['arg_types']:
|
|
397
|
+
if action['arg_types'][arg] == '':
|
|
398
|
+
config_type_dict[key] = "any"
|
|
399
|
+
else:
|
|
400
|
+
config_type_dict[key] = action['arg_types'][arg]
|
|
376
401
|
else:
|
|
377
|
-
config_type_dict[
|
|
378
|
-
else:
|
|
379
|
-
config_type_dict[args[arg][1:]] = "any"
|
|
402
|
+
config_type_dict[key] = "any"
|
|
380
403
|
# todo
|
|
381
404
|
return configure, config_type_dict
|
|
382
405
|
|
|
@@ -387,7 +410,7 @@ class Script(db.Model):
|
|
|
387
410
|
"""
|
|
388
411
|
|
|
389
412
|
return_list = set([action['return'] for action in self.script_dict['script'] if not action['return'] == ''])
|
|
390
|
-
output_str = "
|
|
413
|
+
output_str = "{"
|
|
391
414
|
for i in return_list:
|
|
392
415
|
output_str += "'" + i + "':" + i + ","
|
|
393
416
|
output_str += "}"
|
|
@@ -419,21 +442,114 @@ class Script(db.Model):
|
|
|
419
442
|
:return: A dict containing script types as keys and lists of function body lines as values.
|
|
420
443
|
"""
|
|
421
444
|
line_collection = {}
|
|
445
|
+
|
|
422
446
|
for stype, func_str in exec_str_collection.items():
|
|
423
447
|
if func_str:
|
|
424
448
|
module = ast.parse(func_str)
|
|
425
|
-
func_def = next(node for node in module.body if isinstance(node, ast.FunctionDef))
|
|
426
449
|
|
|
427
|
-
#
|
|
428
|
-
|
|
429
|
-
|
|
450
|
+
# Find the first function (regular or async)
|
|
451
|
+
func_def = next(
|
|
452
|
+
node for node in module.body
|
|
453
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Extract function body as source lines, skipping 'return' nodes
|
|
457
|
+
line_collection[stype] = [
|
|
458
|
+
ast_unparse(node) for node in func_def.body if not isinstance(node, ast.Return)
|
|
459
|
+
]
|
|
430
460
|
return line_collection
|
|
431
461
|
|
|
432
|
-
def
|
|
462
|
+
def render_script_lines(self, script_dict):
|
|
463
|
+
"""
|
|
464
|
+
Convert the script_dict structure into a dict of displayable Python-like lines,
|
|
465
|
+
keeping ID consistency for highlighting.
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
def render_args(args):
|
|
469
|
+
if not args:
|
|
470
|
+
return ""
|
|
471
|
+
return ", ".join(f"{k}={v}" for k, v in args.items())
|
|
472
|
+
|
|
473
|
+
def parse_block(block):
|
|
474
|
+
lines = []
|
|
475
|
+
indent = 0
|
|
476
|
+
stack = []
|
|
477
|
+
|
|
478
|
+
for action in block:
|
|
479
|
+
act = action["action"]
|
|
480
|
+
_id = action["id"]
|
|
481
|
+
instrument = action["instrument"]
|
|
482
|
+
|
|
483
|
+
# Handle control structures
|
|
484
|
+
if act == "if":
|
|
485
|
+
stmt = action["args"].get("statement", "")
|
|
486
|
+
lines.append(" " * indent + f"if {stmt}:")
|
|
487
|
+
indent += 1
|
|
488
|
+
stack.append("if")
|
|
489
|
+
|
|
490
|
+
elif act == "else":
|
|
491
|
+
indent -= 1
|
|
492
|
+
lines.append(" " * indent + f"else:")
|
|
493
|
+
indent += 1
|
|
494
|
+
|
|
495
|
+
elif act in ("endif", "endwhile"):
|
|
496
|
+
if stack:
|
|
497
|
+
stack.pop()
|
|
498
|
+
indent = max(indent - 1, 0)
|
|
499
|
+
lines.append(" " * indent + f"# {act}")
|
|
500
|
+
|
|
501
|
+
elif act == "while":
|
|
502
|
+
stmt = action["args"].get("statement", "")
|
|
503
|
+
lines.append(" " * indent + f"while {stmt}:")
|
|
504
|
+
indent += 1
|
|
505
|
+
stack.append("while")
|
|
506
|
+
|
|
507
|
+
elif act == "wait":
|
|
508
|
+
stmt = action["args"].get("statement", "")
|
|
509
|
+
lines.append(" " * indent + f"time.sleep({stmt})")
|
|
510
|
+
|
|
511
|
+
elif instrument == "variable":
|
|
512
|
+
stmt = action["args"].get("statement", "")
|
|
513
|
+
lines.append(" " * indent + f"{act} = {stmt}")
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
else:
|
|
517
|
+
# Regular function call
|
|
518
|
+
instr = action["instrument"]
|
|
519
|
+
args = render_args(action.get("args", {}))
|
|
520
|
+
ret = action.get("return")
|
|
521
|
+
line = " " * indent
|
|
522
|
+
if ret:
|
|
523
|
+
line += f"{ret} = {instr}.{act}({args})"
|
|
524
|
+
else:
|
|
525
|
+
line += f"{instr}.{act}({args})"
|
|
526
|
+
lines.append(line)
|
|
527
|
+
|
|
528
|
+
# Ensure empty control blocks get "pass"
|
|
529
|
+
final_lines = []
|
|
530
|
+
for i, line in enumerate(lines):
|
|
531
|
+
final_lines.append(line)
|
|
532
|
+
# if line.strip().startswith("else") and (
|
|
533
|
+
# i == len(lines) - 1 or lines[i + 1].startswith("#") or "endif" in lines[i + 1]
|
|
534
|
+
# ):
|
|
535
|
+
# final_lines.append(" " * (indent) + "pass")
|
|
536
|
+
|
|
537
|
+
return final_lines
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
"prep": parse_block(script_dict.get("prep", [])),
|
|
541
|
+
"script": parse_block(script_dict.get("script", [])),
|
|
542
|
+
"cleanup": parse_block(script_dict.get("cleanup", []))
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
def compile(self, script_path=None, batch=False, mode="sample"):
|
|
433
546
|
"""
|
|
434
547
|
Compile the current script to a Python file.
|
|
435
548
|
:return: String to write to a Python file.
|
|
436
549
|
"""
|
|
550
|
+
self.needs_call_human = False
|
|
551
|
+
self.blocks_included = False
|
|
552
|
+
|
|
437
553
|
self.sort_actions()
|
|
438
554
|
run_name = self.name if self.name else "untitled"
|
|
439
555
|
run_name = self.validate_function_name(run_name)
|
|
@@ -441,7 +557,8 @@ class Script(db.Model):
|
|
|
441
557
|
|
|
442
558
|
for i in self.stypes:
|
|
443
559
|
if self.script_dict[i]:
|
|
444
|
-
|
|
560
|
+
is_async = any(a.get("coroutine", False) for a in self.script_dict[i])
|
|
561
|
+
func_str = self._generate_function_header(run_name, i, is_async, batch) + self._generate_function_body(i, batch, mode)
|
|
445
562
|
exec_str_collection[i] = func_str
|
|
446
563
|
if script_path:
|
|
447
564
|
self._write_to_file(script_path, run_name, exec_str_collection)
|
|
@@ -459,50 +576,70 @@ class Script(db.Model):
|
|
|
459
576
|
name += '_'
|
|
460
577
|
return name
|
|
461
578
|
|
|
462
|
-
def _generate_function_header(self, run_name, stype):
|
|
579
|
+
def _generate_function_header(self, run_name, stype, is_async, batch=False):
|
|
463
580
|
"""
|
|
464
581
|
Generate the function header.
|
|
465
582
|
"""
|
|
466
583
|
configure, config_type = self.config(stype)
|
|
467
|
-
|
|
468
|
-
configure = [param + f":{param_type}" if not param_type == "any" else "" for param, param_type in
|
|
584
|
+
configure = [param + f":{param_type}" if not param_type == "any" else param for param, param_type in
|
|
469
585
|
config_type.items()]
|
|
470
586
|
|
|
471
587
|
script_type = f"_{stype}" if stype != "script" else ""
|
|
472
|
-
|
|
588
|
+
async_str = "async " if is_async else ""
|
|
589
|
+
function_header = f"{async_str}def {run_name}{script_type}("
|
|
473
590
|
|
|
474
591
|
if stype == "script":
|
|
475
|
-
|
|
476
|
-
|
|
592
|
+
if batch:
|
|
593
|
+
function_header += "param_list" if configure else "n: int"
|
|
594
|
+
else:
|
|
595
|
+
function_header += ", ".join(configure)
|
|
477
596
|
function_header += "):"
|
|
478
|
-
|
|
597
|
+
|
|
598
|
+
if stype == "script" and batch:
|
|
599
|
+
function_header += self.indent(1) + f'"""Batch mode is experimental and may have bugs."""'
|
|
479
600
|
return function_header
|
|
480
601
|
|
|
481
|
-
def _generate_function_body(self, stype):
|
|
602
|
+
def _generate_function_body(self, stype, batch=False, mode="sample"):
|
|
482
603
|
"""
|
|
483
604
|
Generate the function body for each type in stypes.
|
|
484
605
|
"""
|
|
485
606
|
body = ''
|
|
486
607
|
indent_unit = 1
|
|
608
|
+
if batch and stype == "script":
|
|
609
|
+
return_str, return_list = self.config_return()
|
|
610
|
+
configure, config_type = self.config(stype)
|
|
611
|
+
if not configure:
|
|
612
|
+
body += self.indent(indent_unit) + "param_list = [{} for _ in range(n)]"
|
|
613
|
+
for index, action in enumerate(self.script_dict[stype]):
|
|
614
|
+
text, indent_unit = self._process_action(indent_unit, action, index, stype, batch)
|
|
615
|
+
body += text
|
|
616
|
+
if return_list:
|
|
617
|
+
# body += self.indent(indent_unit) + f"result_list.append({return_str})"
|
|
618
|
+
body += self.indent(indent_unit) + "return param_list"
|
|
487
619
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
620
|
+
else:
|
|
621
|
+
for index, action in enumerate(self.script_dict[stype]):
|
|
622
|
+
text, indent_unit = self._process_action(indent_unit, action, index, stype)
|
|
623
|
+
body += text
|
|
624
|
+
return_str, return_list = self.config_return()
|
|
625
|
+
if return_list and stype == "script":
|
|
626
|
+
body += self.indent(indent_unit) + f"return {return_str}"
|
|
494
627
|
return body
|
|
495
628
|
|
|
496
|
-
def _process_action(self, indent_unit, action, index, stype):
|
|
629
|
+
def _process_action(self, indent_unit, action, index, stype, batch=False, mode="sample"):
|
|
497
630
|
"""
|
|
498
631
|
Process each action within the script dictionary.
|
|
499
632
|
"""
|
|
633
|
+
configure, config_type = self.config(stype)
|
|
634
|
+
|
|
500
635
|
instrument = action['instrument']
|
|
501
636
|
statement = action['args'].get('statement')
|
|
502
637
|
args = self._process_args(action['args'])
|
|
503
638
|
|
|
504
639
|
save_data = action['return']
|
|
505
640
|
action_name = action['action']
|
|
641
|
+
batch_action = action.get("batch_action", False)
|
|
642
|
+
|
|
506
643
|
next_action = self._get_next_action(stype, index)
|
|
507
644
|
# print(args)
|
|
508
645
|
if instrument == 'if':
|
|
@@ -510,16 +647,29 @@ class Script(db.Model):
|
|
|
510
647
|
elif instrument == 'while':
|
|
511
648
|
return self._process_while(indent_unit, action_name, statement, next_action)
|
|
512
649
|
elif instrument == 'variable':
|
|
650
|
+
if batch:
|
|
651
|
+
return self.indent(indent_unit) + "for param in param_list:" + self.indent(indent_unit + 1) + f"param['{action_name}'] = {statement}", indent_unit
|
|
652
|
+
|
|
513
653
|
return self.indent(indent_unit) + f"{action_name} = {statement}", indent_unit
|
|
514
654
|
elif instrument == 'wait':
|
|
655
|
+
if batch:
|
|
656
|
+
return f"{self.indent(indent_unit)}for param in param_list:" + f"{self.indent(indent_unit+1)}time.sleep({statement})", indent_unit
|
|
515
657
|
return f"{self.indent(indent_unit)}time.sleep({statement})", indent_unit
|
|
516
658
|
elif instrument == 'repeat':
|
|
659
|
+
|
|
517
660
|
return self._process_repeat(indent_unit, action_name, statement, next_action)
|
|
518
|
-
|
|
661
|
+
elif instrument == 'pause':
|
|
662
|
+
self.needs_call_human = True
|
|
663
|
+
if batch:
|
|
664
|
+
return f"{self.indent(indent_unit)}for param in param_list:" + f"{self.indent(indent_unit+1)}pause('{statement}')", indent_unit
|
|
665
|
+
return f"{self.indent(indent_unit)}pause('{statement}')", indent_unit
|
|
666
|
+
# todo
|
|
519
667
|
# elif instrument == 'registered_workflows':
|
|
520
668
|
# return inspect.getsource(my_function)
|
|
521
669
|
else:
|
|
522
|
-
|
|
670
|
+
is_async = action.get("coroutine", False)
|
|
671
|
+
dynamic_arg = len(self.get_variables()) > 0
|
|
672
|
+
return self._process_instrument_action(indent_unit, instrument, action_name, args, save_data, is_async, dynamic_arg, batch, batch_action)
|
|
523
673
|
|
|
524
674
|
def _process_args(self, args):
|
|
525
675
|
"""
|
|
@@ -579,23 +729,44 @@ class Script(db.Model):
|
|
|
579
729
|
indent_unit -= 1
|
|
580
730
|
return exec_string, indent_unit
|
|
581
731
|
|
|
582
|
-
def _process_instrument_action(self, indent_unit, instrument, action, args, save_data
|
|
732
|
+
def _process_instrument_action(self, indent_unit, instrument, action, args, save_data, is_async=False, dynamic_arg=False,
|
|
733
|
+
batch=False, batch_action=False):
|
|
583
734
|
"""
|
|
584
735
|
Process actions related to instruments.
|
|
585
736
|
"""
|
|
737
|
+
async_str = "await " if is_async else ""
|
|
586
738
|
|
|
587
|
-
|
|
739
|
+
function_call = f"{instrument}.{action}"
|
|
740
|
+
if instrument.startswith("blocks"):
|
|
741
|
+
self.blocks_included = True
|
|
742
|
+
function_call = action
|
|
743
|
+
|
|
744
|
+
if isinstance(args, dict) and args != {}:
|
|
588
745
|
args_str = self._process_dict_args(args)
|
|
589
|
-
single_line = f"{
|
|
746
|
+
single_line = f"{async_str}{function_call}(**{args_str})"
|
|
590
747
|
elif isinstance(args, str):
|
|
591
|
-
single_line = f"{
|
|
748
|
+
single_line = f"{function_call} = {args}"
|
|
592
749
|
else:
|
|
593
|
-
single_line = f"{
|
|
750
|
+
single_line = f"{async_str}{function_call}()"
|
|
751
|
+
|
|
594
752
|
|
|
595
|
-
if save_data
|
|
596
|
-
|
|
753
|
+
save_data_str = save_data + " = " if save_data else ''
|
|
754
|
+
|
|
755
|
+
if batch and not batch_action:
|
|
756
|
+
arg_list = [args[arg][1:] for arg in args if isinstance(args[arg], str) and args[arg].startswith("#")]
|
|
757
|
+
param_str = [f"param['{arg_list}']" for arg_list in arg_list if arg_list]
|
|
758
|
+
args_str = self.indent(indent_unit + 1) + ", ".join(arg_list) + " = " + ", ".join(param_str) if arg_list else ""
|
|
759
|
+
if dynamic_arg:
|
|
760
|
+
for_string = self.indent(indent_unit) + "for param in param_list:" + args_str
|
|
761
|
+
else:
|
|
762
|
+
for_string = self.indent(indent_unit) + "for i in range(n):"
|
|
763
|
+
output_code = for_string + self.indent(indent_unit + 1) + save_data_str + single_line
|
|
764
|
+
if save_data:
|
|
765
|
+
output_code = output_code + self.indent(indent_unit + 1) + f"param['{save_data}'] = {save_data}"
|
|
766
|
+
else:
|
|
767
|
+
output_code = self.indent(indent_unit) + save_data_str + single_line
|
|
597
768
|
|
|
598
|
-
return
|
|
769
|
+
return output_code, indent_unit
|
|
599
770
|
|
|
600
771
|
def _process_dict_args(self, args):
|
|
601
772
|
"""
|
|
@@ -607,12 +778,21 @@ class Script(db.Model):
|
|
|
607
778
|
args_str = args_str.replace(f"'#{args[arg][1:]}'", args[arg][1:])
|
|
608
779
|
elif isinstance(args[arg], dict):
|
|
609
780
|
# print(args[arg])
|
|
610
|
-
|
|
781
|
+
if not args[arg]:
|
|
782
|
+
continue
|
|
783
|
+
# Extract the variable name (first key in the dict)
|
|
611
784
|
value = next(iter(args[arg]))
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
785
|
+
var_type = args[arg].get(value)
|
|
786
|
+
|
|
787
|
+
# Only process if it's a function_output variable reference
|
|
788
|
+
if var_type == "function_output":
|
|
789
|
+
variables = self.get_variables()
|
|
790
|
+
if value not in variables:
|
|
791
|
+
raise ValueError(f"Variable ({value}) is not defined.")
|
|
792
|
+
# Replace the dict string representation with just the variable name
|
|
793
|
+
args_str = args_str.replace(f"{args[arg]}", value)
|
|
794
|
+
|
|
795
|
+
# elif self._is_variable(arg):
|
|
616
796
|
# print("is variable")
|
|
617
797
|
# args_str = args_str.replace(f"'{args[arg]}'", args[arg])
|
|
618
798
|
return args_str
|
|
@@ -631,7 +811,7 @@ class Script(db.Model):
|
|
|
631
811
|
"""
|
|
632
812
|
return arg in self.script_dict and self.script_dict[arg].get("arg_types") == "variable"
|
|
633
813
|
|
|
634
|
-
def _write_to_file(self, script_path, run_name, exec_string):
|
|
814
|
+
def _write_to_file(self, script_path, run_name, exec_string, call_human=False):
|
|
635
815
|
"""
|
|
636
816
|
Write the compiled script to a file.
|
|
637
817
|
"""
|
|
@@ -641,10 +821,30 @@ class Script(db.Model):
|
|
|
641
821
|
else:
|
|
642
822
|
s.write("deck = None")
|
|
643
823
|
s.write("\nimport time")
|
|
824
|
+
if self.blocks_included:
|
|
825
|
+
s.write(f"\n{self._create_block_import()}")
|
|
826
|
+
if self.needs_call_human:
|
|
827
|
+
s.write("""\n\ndef pause(reason="Manual intervention required"):\n\tprint(f"\\nHUMAN INTERVENTION REQUIRED: {reason}")\n\tinput("Press Enter to continue...\\n")""")
|
|
828
|
+
|
|
644
829
|
for i in exec_string.values():
|
|
645
830
|
s.write(f"\n\n\n{i}")
|
|
646
831
|
|
|
832
|
+
def _create_block_import(self):
|
|
833
|
+
imports = {}
|
|
834
|
+
from ivoryos.utils.decorators import BUILDING_BLOCKS
|
|
835
|
+
for category, methods in BUILDING_BLOCKS.items():
|
|
836
|
+
for method_name, meta in methods.items():
|
|
837
|
+
func = meta["func"]
|
|
838
|
+
module = meta["path"]
|
|
839
|
+
name = func.__name__
|
|
840
|
+
imports.setdefault(module, set()).add(name)
|
|
841
|
+
lines = []
|
|
842
|
+
for module, funcs in imports.items():
|
|
843
|
+
lines.append(f"from {module} import {', '.join(sorted(funcs))}")
|
|
844
|
+
return "\n".join(lines)
|
|
845
|
+
|
|
647
846
|
class WorkflowRun(db.Model):
|
|
847
|
+
"""Represents the entire experiment"""
|
|
648
848
|
__tablename__ = 'workflow_runs'
|
|
649
849
|
|
|
650
850
|
id = db.Column(db.Integer, primary_key=True)
|
|
@@ -653,30 +853,65 @@ class WorkflowRun(db.Model):
|
|
|
653
853
|
start_time = db.Column(db.DateTime, default=datetime.now())
|
|
654
854
|
end_time = db.Column(db.DateTime)
|
|
655
855
|
data_path = db.Column(db.String(256))
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
856
|
+
repeat_mode = db.Column(db.String(64), default="none") # static_repeat, sweep, optimizer
|
|
857
|
+
|
|
858
|
+
# A run contains multiple iterations
|
|
859
|
+
phases = db.relationship(
|
|
860
|
+
'WorkflowPhase',
|
|
861
|
+
backref='workflow_runs', # Clearer back-reference name
|
|
659
862
|
cascade='all, delete-orphan',
|
|
660
|
-
|
|
863
|
+
lazy='dynamic' # Good for handling many iterations
|
|
661
864
|
)
|
|
662
865
|
def as_dict(self):
|
|
663
866
|
dict = self.__dict__
|
|
664
867
|
dict.pop('_sa_instance_state', None)
|
|
665
868
|
return dict
|
|
666
869
|
|
|
870
|
+
class WorkflowPhase(db.Model):
|
|
871
|
+
"""Represents a single function call within a WorkflowRun."""
|
|
872
|
+
__tablename__ = 'workflow_phases'
|
|
873
|
+
|
|
874
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
875
|
+
# Foreign key to link this iteration to its parent run
|
|
876
|
+
run_id = db.Column(db.Integer, db.ForeignKey('workflow_runs.id', ondelete='CASCADE'), nullable=False)
|
|
877
|
+
|
|
878
|
+
# NEW: Store iteration-specific parameters here
|
|
879
|
+
name = db.Column(db.String(64), nullable=False) # 'prep', 'main', 'cleanup'
|
|
880
|
+
repeat_index = db.Column(db.Integer, default=0)
|
|
881
|
+
|
|
882
|
+
parameters = db.Column(JSONType) # Use db.JSON for general support
|
|
883
|
+
outputs = db.Column(JSONType)
|
|
884
|
+
start_time = db.Column(db.DateTime, default=datetime.now)
|
|
885
|
+
end_time = db.Column(db.DateTime)
|
|
886
|
+
|
|
887
|
+
# An iteration contains multiple steps
|
|
888
|
+
steps = db.relationship(
|
|
889
|
+
'WorkflowStep',
|
|
890
|
+
backref='workflow_phases', # Clearer back-reference name
|
|
891
|
+
cascade='all, delete-orphan'
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
def as_dict(self):
|
|
895
|
+
dict = self.__dict__.copy()
|
|
896
|
+
dict.pop('_sa_instance_state', None)
|
|
897
|
+
return dict
|
|
898
|
+
|
|
667
899
|
class WorkflowStep(db.Model):
|
|
668
900
|
__tablename__ = 'workflow_steps'
|
|
669
901
|
|
|
670
902
|
id = db.Column(db.Integer, primary_key=True)
|
|
671
|
-
workflow_id = db.Column(db.Integer, db.ForeignKey('workflow_runs.id', ondelete='CASCADE'), nullable=
|
|
903
|
+
# workflow_id = db.Column(db.Integer, db.ForeignKey('workflow_runs.id', ondelete='CASCADE'), nullable=True)
|
|
904
|
+
phase_id = db.Column(db.Integer, db.ForeignKey('workflow_phases.id', ondelete='CASCADE'), nullable=True)
|
|
672
905
|
|
|
673
|
-
phase = db.Column(db.String(64), nullable=False) # 'prep', 'main', 'cleanup'
|
|
674
|
-
repeat_index = db.Column(db.Integer, default=0) # Only applies to 'main' phase
|
|
906
|
+
# phase = db.Column(db.String(64), nullable=False) # 'prep', 'main', 'cleanup'
|
|
907
|
+
# repeat_index = db.Column(db.Integer, default=0) # Only applies to 'main' phase
|
|
675
908
|
step_index = db.Column(db.Integer, default=0)
|
|
676
909
|
method_name = db.Column(db.String(128), nullable=False)
|
|
677
910
|
start_time = db.Column(db.DateTime)
|
|
678
911
|
end_time = db.Column(db.DateTime)
|
|
679
912
|
run_error = db.Column(db.Boolean, default=False)
|
|
913
|
+
output = db.Column(JSONType, default={})
|
|
914
|
+
# Using as_dict method from ModelBase
|
|
680
915
|
|
|
681
916
|
def as_dict(self):
|
|
682
917
|
dict = self.__dict__.copy()
|
|
@@ -693,7 +928,7 @@ class SingleStep(db.Model):
|
|
|
693
928
|
start_time = db.Column(db.DateTime)
|
|
694
929
|
end_time = db.Column(db.DateTime)
|
|
695
930
|
run_error = db.Column(db.String(128))
|
|
696
|
-
output = db.Column(JSONType)
|
|
931
|
+
output = db.Column(JSONType, nullable=True)
|
|
697
932
|
|
|
698
933
|
def as_dict(self):
|
|
699
934
|
dict = self.__dict__.copy()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
BUILDING_BLOCKS = {}
|
|
5
|
+
|
|
6
|
+
def block(_func=None, *, category="general"):
|
|
7
|
+
def decorator(func):
|
|
8
|
+
if category not in BUILDING_BLOCKS:
|
|
9
|
+
BUILDING_BLOCKS[category] = {}
|
|
10
|
+
if func.__module__ == "__main__":
|
|
11
|
+
file_path = inspect.getfile(func) # e.g. /path/to/math_blocks.py
|
|
12
|
+
module = os.path.splitext(os.path.basename(file_path))[0]
|
|
13
|
+
else:
|
|
14
|
+
module = func.__module__
|
|
15
|
+
BUILDING_BLOCKS[category][func.__name__] = {
|
|
16
|
+
"func": func,
|
|
17
|
+
"signature": inspect.signature(func),
|
|
18
|
+
"docstring": inspect.getdoc(func),
|
|
19
|
+
"coroutine": inspect.iscoroutinefunction(func),
|
|
20
|
+
"path": module
|
|
21
|
+
}
|
|
22
|
+
return func
|
|
23
|
+
if _func is None:
|
|
24
|
+
return decorator
|
|
25
|
+
else:
|
|
26
|
+
return decorator(_func)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BlockNamespace:
|
|
30
|
+
"""[not in use] Expose methods for one block category as attributes."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, methods):
|
|
33
|
+
for name, meta in methods.items():
|
|
34
|
+
setattr(self, name, meta["func"])
|