ivoryos 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ivoryos might be problematic. Click here for more details.
- ivoryos/__init__.py +94 -0
- ivoryos/config.py +46 -0
- ivoryos/routes/__init__.py +0 -0
- ivoryos/routes/auth/__init__.py +0 -0
- ivoryos/routes/auth/auth.py +65 -0
- ivoryos/routes/auth/templates/auth/login.html +25 -0
- ivoryos/routes/auth/templates/auth/signup.html +32 -0
- ivoryos/routes/control/__init__.py +0 -0
- ivoryos/routes/control/control.py +233 -0
- ivoryos/routes/control/templates/control/controllers.html +71 -0
- ivoryos/routes/control/templates/control/controllers_home.html +50 -0
- ivoryos/routes/control/templates/control/controllers_new.html +89 -0
- ivoryos/routes/database/__init__.py +0 -0
- ivoryos/routes/database/database.py +122 -0
- ivoryos/routes/database/templates/database/experiment_database.html +72 -0
- ivoryos/routes/design/__init__.py +0 -0
- ivoryos/routes/design/design.py +396 -0
- ivoryos/routes/design/templates/design/experiment_builder.html +413 -0
- ivoryos/routes/design/templates/design/experiment_run.html +325 -0
- ivoryos/routes/main/__init__.py +0 -0
- ivoryos/routes/main/main.py +25 -0
- ivoryos/routes/main/templates/main/help.html +144 -0
- ivoryos/routes/main/templates/main/home.html +68 -0
- ivoryos/static/favicon.ico +0 -0
- ivoryos/static/gui_annotation/Slide1.png +0 -0
- ivoryos/static/gui_annotation/Slide2.PNG +0 -0
- ivoryos/static/js/overlay.js +12 -0
- ivoryos/static/js/socket_handler.js +25 -0
- ivoryos/static/js/sortable_card.js +24 -0
- ivoryos/static/js/sortable_design.js +36 -0
- ivoryos/static/logo.png +0 -0
- ivoryos/static/style.css +202 -0
- ivoryos/templates/base.html +141 -0
- ivoryos/utils/__init__.py +0 -0
- ivoryos/utils/db_models.py +501 -0
- ivoryos/utils/form.py +316 -0
- ivoryos/utils/global_config.py +68 -0
- ivoryos/utils/llm_agent.py +183 -0
- ivoryos/utils/script_runner.py +158 -0
- ivoryos/utils/task_manager.py +80 -0
- ivoryos/utils/utils.py +337 -0
- ivoryos-0.1.5.dist-info/LICENSE +21 -0
- ivoryos-0.1.5.dist-info/METADATA +96 -0
- ivoryos-0.1.5.dist-info/RECORD +46 -0
- ivoryos-0.1.5.dist-info/WHEEL +5 -0
- ivoryos-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import csv
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from ivoryos.utils import utils
|
|
8
|
+
from ivoryos.utils.db_models import Script
|
|
9
|
+
from ivoryos.utils.global_config import GlobalConfig
|
|
10
|
+
|
|
11
|
+
global_config = GlobalConfig()
|
|
12
|
+
global deck
|
|
13
|
+
deck = None
|
|
14
|
+
|
|
15
|
+
class ScriptRunner:
|
|
16
|
+
def __init__(self, globals_dict=None):
|
|
17
|
+
if globals_dict is None:
|
|
18
|
+
globals_dict = globals()
|
|
19
|
+
self.globals_dict = globals_dict
|
|
20
|
+
|
|
21
|
+
self.stop_event = threading.Event()
|
|
22
|
+
self.is_running = False
|
|
23
|
+
self.lock = threading.Lock()
|
|
24
|
+
|
|
25
|
+
def reset_stop_event(self):
|
|
26
|
+
self.stop_event.clear()
|
|
27
|
+
|
|
28
|
+
def stop_execution(self):
|
|
29
|
+
self.stop_event.set()
|
|
30
|
+
# print("Stop pending tasks")
|
|
31
|
+
|
|
32
|
+
def run_script(self, script, repeat_count=1, run_name=None, logger=None, socketio=None, config=None, bo_args=None,
|
|
33
|
+
output_path=""):
|
|
34
|
+
global deck
|
|
35
|
+
if deck is None:
|
|
36
|
+
deck = global_config.deck
|
|
37
|
+
exec_string = script.compile()
|
|
38
|
+
exec(exec_string)
|
|
39
|
+
time.sleep(1)
|
|
40
|
+
with self.lock:
|
|
41
|
+
if self.is_running:
|
|
42
|
+
logger.info("System is busy. Please wait for it to finish or stop it before starting a new one.")
|
|
43
|
+
return None
|
|
44
|
+
self.is_running = True
|
|
45
|
+
|
|
46
|
+
self.reset_stop_event()
|
|
47
|
+
|
|
48
|
+
thread = threading.Thread(target=self._run_with_stop_check,
|
|
49
|
+
args=(script, repeat_count, run_name, logger, socketio, config, bo_args, output_path))
|
|
50
|
+
thread.start()
|
|
51
|
+
return thread
|
|
52
|
+
|
|
53
|
+
def _run_with_stop_check(self, script: Script, repeat_count, run_name, logger, socketio, config, bo_args,
|
|
54
|
+
output_path):
|
|
55
|
+
time.sleep(1)
|
|
56
|
+
try:
|
|
57
|
+
# Run "prep" section once
|
|
58
|
+
script_dict = script.script_dict
|
|
59
|
+
self._run_actions(script_dict.get("prep", []), section_name="prep", run_name=run_name, logger=logger)
|
|
60
|
+
output_list = []
|
|
61
|
+
_, arg_type = script.config("script")
|
|
62
|
+
_, return_list = script.config_return()
|
|
63
|
+
# Run "script" section multiple times
|
|
64
|
+
if repeat_count:
|
|
65
|
+
self._run_repeat_section(repeat_count, bo_args, output_list, return_list, run_name, logger, socketio)
|
|
66
|
+
elif config:
|
|
67
|
+
self._run_config_section(config, arg_type, output_list, return_list, run_name, logger, socketio)
|
|
68
|
+
# Run "cleanup" section once
|
|
69
|
+
self._run_actions(script_dict.get("cleanup", []), section_name="cleanup", run_name=run_name, logger=logger)
|
|
70
|
+
# Save results if necessary
|
|
71
|
+
if output_list:
|
|
72
|
+
self._save_results(run_name, arg_type, return_list, output_list, logger, output_path)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"Error during script execution: {e}")
|
|
75
|
+
finally:
|
|
76
|
+
with self.lock:
|
|
77
|
+
self.is_running = False # Reset the running flag when done
|
|
78
|
+
|
|
79
|
+
def _run_actions(self, actions, section_name="", run_name=None, logger=None):
|
|
80
|
+
logger.info(f'Executing {section_name} steps') if actions else logger.info(f'No {section_name} steps')
|
|
81
|
+
for action in actions:
|
|
82
|
+
if self.stop_event.is_set():
|
|
83
|
+
logger.info(f"Stopping execution during {section_name} section.")
|
|
84
|
+
break
|
|
85
|
+
logger.info(f'Executing {action.get("action", "")} action')
|
|
86
|
+
fname = f"{run_name}_{section_name}"
|
|
87
|
+
function = self.globals_dict[fname]
|
|
88
|
+
function()
|
|
89
|
+
|
|
90
|
+
def _run_config_section(self, config, arg_type, output_list, return_list, run_name, logger, socketio):
|
|
91
|
+
compiled = True
|
|
92
|
+
for i in config:
|
|
93
|
+
try:
|
|
94
|
+
i = utils.convert_config_type(i, arg_type)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.info(e)
|
|
97
|
+
compiled = False
|
|
98
|
+
break
|
|
99
|
+
if compiled:
|
|
100
|
+
for i, kwargs in enumerate(config):
|
|
101
|
+
kwargs = dict(kwargs)
|
|
102
|
+
if self.stop_event.is_set():
|
|
103
|
+
logger.info(f'Stopping execution during {run_name}: {i + 1}/{len(config)}')
|
|
104
|
+
break
|
|
105
|
+
logger.info(f'Executing {i + 1} of {len(config)} with kwargs = {kwargs}')
|
|
106
|
+
progress = (i + 1) * 100 / len(config)
|
|
107
|
+
socketio.emit('progress', {'progress': progress})
|
|
108
|
+
fname = f"{run_name}_script"
|
|
109
|
+
function = self.globals_dict[fname]
|
|
110
|
+
output = function(**kwargs)
|
|
111
|
+
if output:
|
|
112
|
+
kwargs.update(output)
|
|
113
|
+
output_list.append(kwargs)
|
|
114
|
+
|
|
115
|
+
def _run_repeat_section(self, repeat_count, bo_args, output_list, return_list, run_name, logger, socketio):
|
|
116
|
+
if bo_args:
|
|
117
|
+
logger.info('Initializing optimizer...')
|
|
118
|
+
ax_client = utils.ax_initiation(bo_args)
|
|
119
|
+
for i in range(int(repeat_count)):
|
|
120
|
+
if self.stop_event.is_set():
|
|
121
|
+
logger.info(f'Stopping execution during {run_name}: {i + 1}/{int(repeat_count)}')
|
|
122
|
+
break
|
|
123
|
+
logger.info(f'Executing {run_name} experiment: {i + 1}/{int(repeat_count)}')
|
|
124
|
+
progress = (i + 1) * 100 / int(repeat_count)
|
|
125
|
+
socketio.emit('progress', {'progress': progress})
|
|
126
|
+
if bo_args:
|
|
127
|
+
try:
|
|
128
|
+
parameters, trial_index = ax_client.get_next_trial()
|
|
129
|
+
logger.info(f'Output value: {parameters}')
|
|
130
|
+
fname = f"{run_name}_script"
|
|
131
|
+
function = self.globals_dict[fname]
|
|
132
|
+
output = function(**parameters)
|
|
133
|
+
# output = eval(f"{run_name}_script(**{parameters})")
|
|
134
|
+
ax_client.complete_trial(trial_index=trial_index, raw_data=output)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.info(f'Optimization error: {e}')
|
|
137
|
+
break
|
|
138
|
+
else:
|
|
139
|
+
fname = f"{run_name}_script"
|
|
140
|
+
function = self.globals_dict[fname]
|
|
141
|
+
output = function()
|
|
142
|
+
|
|
143
|
+
if output:
|
|
144
|
+
output_list.append(output)
|
|
145
|
+
logger.info(f'Output value: {output}')
|
|
146
|
+
return output_list
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _save_results(run_name, arg_type, return_list, output_list, logger, output_path):
|
|
150
|
+
args = list(arg_type.keys()) if arg_type else []
|
|
151
|
+
args.extend(return_list)
|
|
152
|
+
filename = run_name + "_" + datetime.now().strftime("%Y-%m-%d %H-%M") + ".csv"
|
|
153
|
+
file_path = os.path.join(output_path, filename)
|
|
154
|
+
with open(file_path, "w", newline='') as file:
|
|
155
|
+
writer = csv.DictWriter(file, fieldnames=args)
|
|
156
|
+
writer.writeheader()
|
|
157
|
+
writer.writerows(output_list)
|
|
158
|
+
logger.info(f'Results saved to {file_path}')
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import queue
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# A task manager class to manage the queue and tasks
|
|
7
|
+
class TaskManager:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.task_queue = queue.Queue()
|
|
10
|
+
self.current_task = None
|
|
11
|
+
self.stop_event = threading.Event()
|
|
12
|
+
|
|
13
|
+
def add_task(self, func, **kwargs):
|
|
14
|
+
# Add the function and its kwargs to the task queue
|
|
15
|
+
self.task_queue.put((func, kwargs))
|
|
16
|
+
|
|
17
|
+
def run_tasks(self):
|
|
18
|
+
# Run the tasks from the queue
|
|
19
|
+
while not self.task_queue.empty():
|
|
20
|
+
func, kwargs = self.task_queue.get()
|
|
21
|
+
thread = threading.Thread(target=self.run_task, args=(func, kwargs))
|
|
22
|
+
self.current_task = thread
|
|
23
|
+
thread.start()
|
|
24
|
+
thread.join() # Wait for the task to finish
|
|
25
|
+
|
|
26
|
+
def run_task(self, func, kwargs):
|
|
27
|
+
# Run the task function with control to stop in the middle
|
|
28
|
+
self.stop_event.clear() # Reset the stop flag
|
|
29
|
+
func(**kwargs)
|
|
30
|
+
if self.stop_event.is_set():
|
|
31
|
+
print("Current task was stopped.")
|
|
32
|
+
|
|
33
|
+
def stop_current_task(self):
|
|
34
|
+
# Stop the current task by setting the stop flag
|
|
35
|
+
if self.current_task and self.current_task.is_alive():
|
|
36
|
+
print("Stopping current task...")
|
|
37
|
+
self.stop_event.set() # Signal to stop the current task
|
|
38
|
+
self.current_task.join() # Wait for the task to stop
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Wrapping tasks to allow stopping between them
|
|
42
|
+
def function_to_call(stop_event, **kwargs):
|
|
43
|
+
if stop_event.is_set():
|
|
44
|
+
return
|
|
45
|
+
task1(kwargs['arg1'])
|
|
46
|
+
if stop_event.is_set():
|
|
47
|
+
return
|
|
48
|
+
task2(kwargs['arg2'])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Dummy task functions as provided
|
|
52
|
+
def task1(arg1):
|
|
53
|
+
for i in range(arg1):
|
|
54
|
+
print(f"Task 1 running: {i}")
|
|
55
|
+
time.sleep(1)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def task2(arg2):
|
|
59
|
+
for i in range(arg2):
|
|
60
|
+
print(f"Task 2 running: {i}")
|
|
61
|
+
time.sleep(1)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
manager = TaskManager()
|
|
66
|
+
|
|
67
|
+
# Add tasks to the manager
|
|
68
|
+
manager.add_task(function_to_call, stop_event=manager.stop_event, arg1=3, arg2=5)
|
|
69
|
+
manager.add_task(function_to_call, stop_event=manager.stop_event, arg1=2, arg2=4)
|
|
70
|
+
|
|
71
|
+
# Run tasks in a separate thread
|
|
72
|
+
manager_thread = threading.Thread(target=manager.run_tasks)
|
|
73
|
+
manager_thread.start()
|
|
74
|
+
|
|
75
|
+
# Example: Stop the current workflow while task1 is running
|
|
76
|
+
time.sleep(2) # Let task1 run for a bit
|
|
77
|
+
manager.stop_current_task()
|
|
78
|
+
|
|
79
|
+
# Wait for all tasks to finish
|
|
80
|
+
manager_thread.join()
|
ivoryos/utils/utils.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import pickle
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Optional, Dict, Tuple
|
|
9
|
+
|
|
10
|
+
from flask import session
|
|
11
|
+
from flask_socketio import SocketIO
|
|
12
|
+
|
|
13
|
+
from ivoryos.utils.db_models import Script
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_script_file():
|
|
17
|
+
session_script = session.get("scripts")
|
|
18
|
+
if session_script:
|
|
19
|
+
s = Script()
|
|
20
|
+
s.__dict__.update(**session_script)
|
|
21
|
+
return s
|
|
22
|
+
else:
|
|
23
|
+
return Script(author=session.get('user'))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def post_script_file(script, is_dict=False):
|
|
27
|
+
if is_dict:
|
|
28
|
+
session['scripts'] = script
|
|
29
|
+
else:
|
|
30
|
+
session['scripts'] = script.as_dict()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_gui_dir(parent_path):
|
|
34
|
+
os.makedirs(parent_path, exist_ok=True)
|
|
35
|
+
for path in ["config_csv", "scripts", "results", "pseudo_deck"]:
|
|
36
|
+
os.makedirs(os.path.join(parent_path, path), exist_ok=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def save_to_history(filepath, history_path):
|
|
40
|
+
connections = []
|
|
41
|
+
try:
|
|
42
|
+
with open(history_path, 'r') as file:
|
|
43
|
+
lines = file.read()
|
|
44
|
+
connections = lines.split('\n')
|
|
45
|
+
except FileNotFoundError:
|
|
46
|
+
pass
|
|
47
|
+
if filepath not in connections:
|
|
48
|
+
with open(history_path, 'a') as file:
|
|
49
|
+
file.writelines(f"{filepath}\n")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def import_history(history_path):
|
|
53
|
+
connections = []
|
|
54
|
+
try:
|
|
55
|
+
with open(history_path, 'r') as file:
|
|
56
|
+
lines = file.read()
|
|
57
|
+
connections = lines.split('\n')
|
|
58
|
+
except FileNotFoundError:
|
|
59
|
+
pass
|
|
60
|
+
connections = [i for i in connections if not i == '']
|
|
61
|
+
return connections
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def available_pseudo_deck(path):
|
|
65
|
+
import os
|
|
66
|
+
return os.listdir(path)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_functions(class_object=None, debug=False):
|
|
70
|
+
functions = {}
|
|
71
|
+
under_score = "_"
|
|
72
|
+
if debug:
|
|
73
|
+
under_score = "__"
|
|
74
|
+
for function, method in inspect.getmembers(type(class_object), predicate=inspect.isfunction):
|
|
75
|
+
if not function.startswith(under_score) and not function.isupper():
|
|
76
|
+
try:
|
|
77
|
+
annotation = inspect.signature(method)
|
|
78
|
+
# if doc_string:
|
|
79
|
+
docstring = inspect.getdoc(method)
|
|
80
|
+
functions[function] = dict(signature=annotation, docstring=docstring)
|
|
81
|
+
|
|
82
|
+
# handle getter setters
|
|
83
|
+
# if callable(att):
|
|
84
|
+
# functions[function] = inspect.signature(att)
|
|
85
|
+
# else:
|
|
86
|
+
# att = getattr(class_object.__class__, function)
|
|
87
|
+
# if isinstance(att, property) and att.fset is not None:
|
|
88
|
+
# setter = att.fset.__annotations__
|
|
89
|
+
# setter.pop('return', None)
|
|
90
|
+
# if setter:
|
|
91
|
+
# functions[function] = setter
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
return functions
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _get_type_from_parameters(arg, parameters):
|
|
98
|
+
arg_type = ''
|
|
99
|
+
if type(parameters) is inspect.Signature:
|
|
100
|
+
p = parameters.parameters
|
|
101
|
+
# print(p[arg].annotation)
|
|
102
|
+
if p[arg].annotation is not inspect._empty:
|
|
103
|
+
# print(p[arg].annotation)
|
|
104
|
+
if p[arg].annotation.__module__ == 'typing':
|
|
105
|
+
# print(p[arg].annotation.__args__)
|
|
106
|
+
arg_type = [i.__name__ for i in p[arg].annotation.__args__]
|
|
107
|
+
else:
|
|
108
|
+
arg_type = p[arg].annotation.__name__
|
|
109
|
+
# print(arg_type)
|
|
110
|
+
elif type(parameters) is dict:
|
|
111
|
+
if parameters[arg]:
|
|
112
|
+
|
|
113
|
+
if parameters[arg].__module__ == 'typing':
|
|
114
|
+
arg_type = [i.__name__ for i in parameters[arg].__args__]
|
|
115
|
+
else:
|
|
116
|
+
arg_type = parameters[arg].__name__
|
|
117
|
+
return arg_type
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def find_variable_in_script(script: Script, args: Dict[str, str]) -> Optional[Tuple[Dict[str, str], Dict[str, str]]]:
|
|
121
|
+
# TODO: need to search for if the variable exists
|
|
122
|
+
added_variables: list[Dict[str, str]] = [action for action in script.currently_editing_script if
|
|
123
|
+
action["instrument"] == "variable"]
|
|
124
|
+
|
|
125
|
+
possible_variable_arguments = {}
|
|
126
|
+
possible_variable_types = {}
|
|
127
|
+
|
|
128
|
+
for arg_name, arg_val in args.items():
|
|
129
|
+
for added_variable in added_variables:
|
|
130
|
+
if added_variable["action"] == arg_val:
|
|
131
|
+
possible_variable_arguments[arg_name] = added_variable["action"]
|
|
132
|
+
possible_variable_types[arg_name] = "variable"
|
|
133
|
+
|
|
134
|
+
return possible_variable_arguments, possible_variable_types
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _convert_by_str(args, arg_types):
|
|
138
|
+
# print(arg_types)
|
|
139
|
+
if type(arg_types) is not list:
|
|
140
|
+
arg_types = [arg_types]
|
|
141
|
+
for i in arg_types:
|
|
142
|
+
if i == "any":
|
|
143
|
+
try:
|
|
144
|
+
args = eval(args)
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
return args
|
|
148
|
+
try:
|
|
149
|
+
args = eval(f'{i}("{args}")')
|
|
150
|
+
return args
|
|
151
|
+
except Exception:
|
|
152
|
+
raise TypeError(f"Input type error: cannot convert '{args}' to {i}.")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _convert_by_class(args, arg_types):
|
|
156
|
+
if arg_types.__module__ == 'builtins':
|
|
157
|
+
args = arg_types(args)
|
|
158
|
+
return args
|
|
159
|
+
elif arg_types.__module__ == "typing":
|
|
160
|
+
for i in arg_types.__args__: # for typing.Union
|
|
161
|
+
try:
|
|
162
|
+
args = i(args)
|
|
163
|
+
return args
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
raise TypeError("Input type error.")
|
|
167
|
+
# else:
|
|
168
|
+
# args = globals()[args]
|
|
169
|
+
return args
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def convert_config_type(args, arg_types, is_class: bool = False):
|
|
173
|
+
bool_dict = {"True": True, "False": False}
|
|
174
|
+
# print(args, arg_types)
|
|
175
|
+
# print(globals())
|
|
176
|
+
if args:
|
|
177
|
+
for arg in args:
|
|
178
|
+
if arg not in arg_types.keys():
|
|
179
|
+
raise ValueError("config file format not supported.")
|
|
180
|
+
if args[arg] == '' or args[arg] == "None":
|
|
181
|
+
args[arg] = None
|
|
182
|
+
elif args[arg] == "True" or args[arg] == "False":
|
|
183
|
+
args[arg] = bool_dict[args[arg]]
|
|
184
|
+
else:
|
|
185
|
+
arg_type = arg_types[arg]
|
|
186
|
+
|
|
187
|
+
if is_class:
|
|
188
|
+
# if arg_type.__module__ == 'builtins':
|
|
189
|
+
args[arg] = _convert_by_class(args[arg], arg_type)
|
|
190
|
+
else:
|
|
191
|
+
args[arg] = _convert_by_str(args[arg], arg_type)
|
|
192
|
+
return args
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def import_module_by_filepath(filepath: str, name: str):
|
|
196
|
+
spec = importlib.util.spec_from_file_location(name, filepath)
|
|
197
|
+
module = importlib.util.module_from_spec(spec)
|
|
198
|
+
spec.loader.exec_module(module)
|
|
199
|
+
return module
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class SocketIOHandler(logging.Handler):
|
|
203
|
+
def __init__(self, socketio: SocketIO):
|
|
204
|
+
super().__init__()
|
|
205
|
+
self.formatter = logging.Formatter('%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
|
206
|
+
self.socketio = socketio
|
|
207
|
+
|
|
208
|
+
def emit(self, record):
|
|
209
|
+
message = self.format(record)
|
|
210
|
+
# session["last_log"] = message
|
|
211
|
+
self.socketio.emit('log', {'message': message})
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def start_logger(socketio: SocketIO, logger_name: str, log_filename: str = None):
|
|
215
|
+
# logging.basicConfig( format='%(asctime)s - %(message)s')
|
|
216
|
+
formatter = logging.Formatter(fmt='%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
|
217
|
+
logger = logging.getLogger(logger_name)
|
|
218
|
+
logger.setLevel(logging.INFO)
|
|
219
|
+
file_handler = logging.FileHandler(filename=log_filename, )
|
|
220
|
+
file_handler.setFormatter(formatter)
|
|
221
|
+
logger.addHandler(file_handler)
|
|
222
|
+
# console_logger = logging.StreamHandler() # stream to console
|
|
223
|
+
# logger.addHandler(console_logger)
|
|
224
|
+
socketio_handler = SocketIOHandler(socketio)
|
|
225
|
+
logger.addHandler(socketio_handler)
|
|
226
|
+
return logger
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def ax_wrapper(data):
|
|
230
|
+
from ax.service.utils.instantiation import ObjectiveProperties
|
|
231
|
+
parameter = []
|
|
232
|
+
objectives = {}
|
|
233
|
+
# Iterate through the webui_data dictionary
|
|
234
|
+
for key, value in data.items():
|
|
235
|
+
# Check if the key corresponds to a parameter type
|
|
236
|
+
if "_type" in key:
|
|
237
|
+
param_name = key.split("_type")[0]
|
|
238
|
+
param_type = value
|
|
239
|
+
param_value = data[f"{param_name}_value"].split(",")
|
|
240
|
+
try:
|
|
241
|
+
values = [float(v) for v in param_value]
|
|
242
|
+
except Exception:
|
|
243
|
+
values = param_value
|
|
244
|
+
if param_type == "range":
|
|
245
|
+
parameter.append({"name": param_name, "type": param_type, "bounds": values})
|
|
246
|
+
if param_type == "choice":
|
|
247
|
+
parameter.append({"name": param_name, "type": param_type, "values": values})
|
|
248
|
+
if param_type == "fixed":
|
|
249
|
+
parameter.append({"name": param_name, "type": param_type, "value": values[0]})
|
|
250
|
+
elif key.endswith("_min"):
|
|
251
|
+
if not value == 'none':
|
|
252
|
+
obj_name = key.split("_min")[0]
|
|
253
|
+
is_min = True if value == "minimize" else False
|
|
254
|
+
|
|
255
|
+
threshold = None if f"{obj_name}_threshold" not in data else data[f"{obj_name}_threshold"]
|
|
256
|
+
properties = ObjectiveProperties(minimize=is_min, threshold=threshold)
|
|
257
|
+
objectives[obj_name] = properties
|
|
258
|
+
return parameter, objectives
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def ax_initiation(data):
|
|
262
|
+
install_and_import("ax", "ax-platform")
|
|
263
|
+
parameter, objectives = ax_wrapper(data)
|
|
264
|
+
from ax.service.ax_client import AxClient
|
|
265
|
+
ax_client = AxClient()
|
|
266
|
+
ax_client.create_experiment(parameter, objectives)
|
|
267
|
+
return ax_client
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def get_arg_type(args, parameters):
|
|
271
|
+
arg_types = {}
|
|
272
|
+
# print(args, parameters)
|
|
273
|
+
if args:
|
|
274
|
+
for arg in args:
|
|
275
|
+
arg_types[arg] = _get_type_from_parameters(arg, parameters)
|
|
276
|
+
return arg_types
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def install_and_import(package, package_name=None):
|
|
280
|
+
try:
|
|
281
|
+
# Check if the package is already installed
|
|
282
|
+
importlib.import_module(package)
|
|
283
|
+
# print(f"{package} is already installed.")
|
|
284
|
+
except ImportError:
|
|
285
|
+
# If not installed, install it
|
|
286
|
+
# print(f"{package} is not installed. Installing now...")
|
|
287
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name or package])
|
|
288
|
+
# print(f"{package} has been installed successfully.")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def process_data(data, config_type):
|
|
292
|
+
rows = {} # Dictionary to hold webui_data organized by rows
|
|
293
|
+
|
|
294
|
+
# Organize webui_data by rows
|
|
295
|
+
for key, value in data.items():
|
|
296
|
+
if value: # Only process non-empty values
|
|
297
|
+
# Extract the field name and row index
|
|
298
|
+
field_name, row_index = key.split('[')
|
|
299
|
+
row_index = int(row_index.rstrip(']'))
|
|
300
|
+
|
|
301
|
+
# If row not in rows, create a new dictionary for that row
|
|
302
|
+
if row_index not in rows:
|
|
303
|
+
rows[row_index] = {}
|
|
304
|
+
|
|
305
|
+
# Add or update the field value in the specific row's dictionary
|
|
306
|
+
rows[row_index][field_name] = value
|
|
307
|
+
|
|
308
|
+
# Filter out any empty rows and create a list of dictionaries
|
|
309
|
+
filtered_rows = [row for row in rows.values() if len(row) == len(config_type)]
|
|
310
|
+
|
|
311
|
+
return filtered_rows
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def parse_deck(deck, save=False, output_path=''):
|
|
315
|
+
deck_variables = {f"deck.{name}": parse_functions(val) for name, val in vars(deck).items()
|
|
316
|
+
if not type(val).__module__ == 'builtins'
|
|
317
|
+
and not name[0].isupper()
|
|
318
|
+
and not name.startswith("_")}
|
|
319
|
+
if deck_variables and save:
|
|
320
|
+
# pseudo_deck = parse_dict
|
|
321
|
+
parse_dict = deck_variables.copy()
|
|
322
|
+
parse_dict["deck_name"] = os.path.splitext(os.path.basename(deck.__file__))[
|
|
323
|
+
0] if deck.__name__ == "__main__" else deck.__name__
|
|
324
|
+
with open(os.path.join(output_path, f"{parse_dict['deck_name']}.pkl"), 'wb') as file:
|
|
325
|
+
pickle.dump(parse_dict, file)
|
|
326
|
+
return deck_variables
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def load_deck(pkl_name):
|
|
330
|
+
if not pkl_name:
|
|
331
|
+
return None
|
|
332
|
+
try:
|
|
333
|
+
with open(pkl_name, 'rb') as f:
|
|
334
|
+
pseudo_deck = pickle.load(f)
|
|
335
|
+
return pseudo_deck
|
|
336
|
+
except FileNotFoundError:
|
|
337
|
+
return None
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-2024 Ivory Zhang | Hein Lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ivoryos
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
|
|
5
|
+
Home-page: https://gitlab.com/heingroup/ivoryos
|
|
6
|
+
Author: Ivory Zhang
|
|
7
|
+
Author-email: ivoryzhang@chem.ubc.ca
|
|
8
|
+
License: MIT
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: bcrypt
|
|
12
|
+
Requires-Dist: Flask-Login
|
|
13
|
+
Requires-Dist: Flask-Session
|
|
14
|
+
Requires-Dist: Flask-SocketIO
|
|
15
|
+
Requires-Dist: Flask-SQLAlchemy
|
|
16
|
+
Requires-Dist: Flask-WTF
|
|
17
|
+
Requires-Dist: SQLAlchemy-Utils
|
|
18
|
+
Requires-Dist: python-dotenv
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
# ivoryOS: interoperable Web UI for self-driving laboratories (SDLs)
|
|
22
|
+
ivoryOS is a "plug and play" web UI extension for flexible SDLs, enabling interoperability between SDLs.
|
|
23
|
+
## Description
|
|
24
|
+
Granting SDLs flexibility and modularity makes it almost impossible to design a UI, yet it's a necessity for allowing more people to interact with it (democratisation).
|
|
25
|
+
This web UI aims to ease up the control of any Python-based SDLs by displaying functions and parameters for initialized modules dynamically.
|
|
26
|
+
The modules can be hardware API, high-level functions, or experiment workflow.
|
|
27
|
+
With the least modification of the current workflow, user can design, manage and execute their experimental designs and monitor the execution process.
|
|
28
|
+
## AI assistant
|
|
29
|
+
To streamline the experimental design on SDLs, we also integrate Large Language Models (LLMs) to interpret the inspected functions and generate code according to task descriptions.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
```
|
|
33
|
+
pip install ivoryos
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
### Quick start
|
|
38
|
+
In your SDL script, use `ivoryos(__name__)`. Example in [abstract_sdl.py](https://gitlab.com/heingroup/ivoryos/-/blob/main/example/dummy_ur/dummy_deck.py)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import ivoryos
|
|
42
|
+
|
|
43
|
+
ivoryos.run(__name__)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Additional settings
|
|
47
|
+
#### Enable LLMs with [OpenAI API](https://github.com/openai/openai-python)
|
|
48
|
+
1. Create a `.env` file for `OPENAI_API_KEY`
|
|
49
|
+
```
|
|
50
|
+
OPENAI_API_KEY="Your API Key"
|
|
51
|
+
```
|
|
52
|
+
2. In your SDL script, define model, you can use any GPT models.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
ivoryos.run(__name__, model="gpt-3.5-turbo")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### Enable local LLMs with [Ollama](https://ollama.com/)
|
|
59
|
+
1. Download Ollama.
|
|
60
|
+
2. pull models from Ollama
|
|
61
|
+
3. In your SDL script, define LLM server and model, you can use any models available on Ollama.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
ivoryos.run(__name__, llm_server="localhost", model="llama3.1")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### Add additional logger(s)
|
|
68
|
+
```python
|
|
69
|
+
ivoryos.run(__name__, logger="logger name")
|
|
70
|
+
```
|
|
71
|
+
or
|
|
72
|
+
```python
|
|
73
|
+
ivoryos.run(__name__, logger=["logger 1", "logger 2"])
|
|
74
|
+
```
|
|
75
|
+
#### Offline (design without hardware connection)
|
|
76
|
+
After one successful connection, a blueprint will be automatically saved and made accessible without hardware connection. In a new Python script in the same directory, use `ivoryos.run()` to start offline mode.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
ivoryos.run()
|
|
80
|
+
```
|
|
81
|
+
## Deck snapshot example
|
|
82
|
+

|
|
83
|
+
## Developing
|
|
84
|
+
This is a wip project. Here are some future actions.
|
|
85
|
+
1. Support @setter decorator.
|
|
86
|
+
2. Documentation: white paper wip
|
|
87
|
+
3. Compatibility: compatability report to open-source lab hardware APIs will soon be added. As of now, due to the limitation of web form, the usability of APIs with object inputs (e.g. Opentron Python API) is very limited.
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## Authors and Acknowledgement
|
|
91
|
+
Ivory Zhang, Lucy Hao
|
|
92
|
+
|
|
93
|
+
Authors acknowledge all former and current Hein Lab members for their valuable suggestions.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
[LICENSE](LICENSE)
|