ivoryos 0.1.5__py3-none-any.whl → 0.1.7__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.
- ivoryos/__init__.py +13 -8
- ivoryos/config.py +1 -0
- ivoryos/routes/control/control.py +70 -31
- ivoryos/routes/control/templates/control/controllers.html +6 -2
- ivoryos/routes/database/database.py +3 -2
- ivoryos/routes/design/design.py +4 -4
- ivoryos/routes/main/templates/main/help.html +0 -3
- ivoryos/static/js/socket_handler.js +10 -1
- ivoryos/static/logo.webp +0 -0
- ivoryos/templates/base.html +4 -1
- ivoryos/utils/db_models.py +7 -8
- ivoryos/utils/form.py +2 -3
- ivoryos/utils/global_config.py +6 -6
- ivoryos/utils/llm_agent.py +1 -1
- ivoryos/utils/script_runner.py +9 -3
- ivoryos/utils/utils.py +131 -46
- ivoryos-0.1.7.dist-info/METADATA +166 -0
- {ivoryos-0.1.5.dist-info → ivoryos-0.1.7.dist-info}/RECORD +21 -20
- ivoryos-0.1.5.dist-info/METADATA +0 -96
- {ivoryos-0.1.5.dist-info → ivoryos-0.1.7.dist-info}/LICENSE +0 -0
- {ivoryos-0.1.5.dist-info → ivoryos-0.1.7.dist-info}/WHEEL +0 -0
- {ivoryos-0.1.5.dist-info → ivoryos-0.1.7.dist-info}/top_level.txt +0 -0
ivoryos/__init__.py
CHANGED
|
@@ -2,7 +2,7 @@ import os
|
|
|
2
2
|
import sys
|
|
3
3
|
from typing import Union
|
|
4
4
|
|
|
5
|
-
from flask import Flask
|
|
5
|
+
from flask import Flask, redirect, url_for
|
|
6
6
|
|
|
7
7
|
from ivoryos.config import Config, get_config
|
|
8
8
|
from ivoryos.routes.auth.auth import auth, login_manager
|
|
@@ -19,7 +19,8 @@ global_config = GlobalConfig()
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def create_app(config_class=None):
|
|
22
|
-
|
|
22
|
+
url_prefix = os.getenv('URL_PREFIX', None)
|
|
23
|
+
app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='static')
|
|
23
24
|
app.config.from_object(config_class or 'config.get_config()')
|
|
24
25
|
|
|
25
26
|
# Initialize extensions
|
|
@@ -45,11 +46,15 @@ def create_app(config_class=None):
|
|
|
45
46
|
g.logger = logger
|
|
46
47
|
g.socketio = socketio
|
|
47
48
|
|
|
48
|
-
app.register_blueprint(main)
|
|
49
|
-
app.register_blueprint(auth)
|
|
50
|
-
app.register_blueprint(design)
|
|
51
|
-
app.register_blueprint(database)
|
|
52
|
-
app.register_blueprint(control)
|
|
49
|
+
app.register_blueprint(main, url_prefix=url_prefix)
|
|
50
|
+
app.register_blueprint(auth, url_prefix=url_prefix)
|
|
51
|
+
app.register_blueprint(design, url_prefix=url_prefix)
|
|
52
|
+
app.register_blueprint(database, url_prefix=url_prefix)
|
|
53
|
+
app.register_blueprint(control, url_prefix=url_prefix)
|
|
54
|
+
|
|
55
|
+
@app.route('/')
|
|
56
|
+
def redirect_to_prefix():
|
|
57
|
+
return redirect(url_for('main.index')) # Assuming 'index' is a route in your blueprint
|
|
53
58
|
|
|
54
59
|
return app
|
|
55
60
|
|
|
@@ -72,7 +77,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
72
77
|
app.config["MODULE"] = module
|
|
73
78
|
app.config["OFF_LINE"] = False
|
|
74
79
|
global_config.deck = sys.modules[module]
|
|
75
|
-
global_config.
|
|
80
|
+
global_config.deck_snapshot = utils.create_deck_snapshot(global_config.deck, output_path=app.config["DUMMY_DECK"], save=True)
|
|
76
81
|
# global_config.runner = ScriptRunner(globals())
|
|
77
82
|
else:
|
|
78
83
|
app.config["OFF_LINE"] = True
|
ivoryos/config.py
CHANGED
|
@@ -15,6 +15,7 @@ class Config:
|
|
|
15
15
|
SCRIPT_FOLDER = os.path.join(OUTPUT_FOLDER, 'scripts/')
|
|
16
16
|
DATA_FOLDER = os.path.join(OUTPUT_FOLDER, 'results/')
|
|
17
17
|
DUMMY_DECK = os.path.join(OUTPUT_FOLDER, 'pseudo_deck/')
|
|
18
|
+
LLM_OUTPUT = os.path.join(OUTPUT_FOLDER, 'llm_output/')
|
|
18
19
|
DECK_HISTORY = os.path.join(OUTPUT_FOLDER, 'deck_history.txt')
|
|
19
20
|
LOGGERS_PATH = "default.log"
|
|
20
21
|
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import pickle
|
|
3
|
-
import sys
|
|
4
2
|
|
|
5
|
-
from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app
|
|
3
|
+
from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app, jsonify
|
|
6
4
|
from flask_login import login_required
|
|
7
5
|
|
|
8
6
|
from ivoryos.utils.global_config import GlobalConfig
|
|
@@ -17,7 +15,7 @@ control = Blueprint('control', __name__, template_folder='templates/control')
|
|
|
17
15
|
@control.route("/my_deck")
|
|
18
16
|
@login_required
|
|
19
17
|
def deck_controllers():
|
|
20
|
-
deck_variables = global_config.
|
|
18
|
+
deck_variables = global_config.deck_snapshot.keys()
|
|
21
19
|
deck_list = utils.import_history(os.path.join(current_app.config["OUTPUT_FOLDER"], 'deck_history.txt'))
|
|
22
20
|
return render_template('controllers_home.html', defined_variables=deck_variables, deck=True, history=deck_list)
|
|
23
21
|
|
|
@@ -77,12 +75,15 @@ def controllers_home():
|
|
|
77
75
|
def controllers(instrument):
|
|
78
76
|
inst_object = find_instrument_by_name(instrument)
|
|
79
77
|
_forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
functions = list(_forms.keys())
|
|
79
|
+
|
|
80
|
+
order = get_session_by_instrument('card_order', instrument)
|
|
81
|
+
hidden_functions = get_session_by_instrument('hide_function', instrument)
|
|
82
|
+
|
|
83
|
+
for function in functions:
|
|
84
|
+
if function not in hidden_functions and function not in order:
|
|
85
|
+
order.append(function)
|
|
86
|
+
post_session_by_instrument('card_order', instrument, order)
|
|
86
87
|
forms = {name: _forms[name] for name in order if name in _forms}
|
|
87
88
|
if request.method == 'POST':
|
|
88
89
|
all_kwargs = request.form.copy()
|
|
@@ -103,6 +104,32 @@ def controllers(instrument):
|
|
|
103
104
|
return render_template('controllers.html', instrument=instrument, forms=forms, format_name=format_name)
|
|
104
105
|
|
|
105
106
|
|
|
107
|
+
@control.route("/backend_control/<instrument>", methods=['GET', 'POST'])
|
|
108
|
+
@login_required
|
|
109
|
+
def backend_control(instrument):
|
|
110
|
+
inst_object = find_instrument_by_name(instrument)
|
|
111
|
+
forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
|
|
112
|
+
if request.method == 'POST':
|
|
113
|
+
all_kwargs = request.form.copy()
|
|
114
|
+
method_name = all_kwargs.pop("hidden_name", None)
|
|
115
|
+
# if method_name is not None:
|
|
116
|
+
form = forms.get(method_name, None)
|
|
117
|
+
kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
|
|
118
|
+
function_executable = getattr(inst_object, method_name)
|
|
119
|
+
if form:
|
|
120
|
+
# print(kwargs)
|
|
121
|
+
try:
|
|
122
|
+
kwargs.pop("hidden_name")
|
|
123
|
+
output = function_executable(**kwargs)
|
|
124
|
+
json_output = jsonify(output)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
json_output = jsonify(e.__str__())
|
|
127
|
+
return json_output, 400
|
|
128
|
+
else:
|
|
129
|
+
return "instrument not exist", 400
|
|
130
|
+
return json_output, 200
|
|
131
|
+
|
|
132
|
+
|
|
106
133
|
@control.route("/import_api", methods=['GET', 'POST'])
|
|
107
134
|
def import_api():
|
|
108
135
|
filepath = request.form.get('filepath')
|
|
@@ -164,12 +191,12 @@ def import_deck():
|
|
|
164
191
|
try:
|
|
165
192
|
module = utils.import_module_by_filepath(filepath=filepath, name=name)
|
|
166
193
|
utils.save_to_history(filepath, current_app.config["DECK_HISTORY"])
|
|
167
|
-
module_sigs = utils.
|
|
194
|
+
module_sigs = utils.create_deck_snapshot(module, save=update, output_path=current_app.config["DUMMY_DECK"])
|
|
168
195
|
if not len(module_sigs) > 0:
|
|
169
196
|
flash("Invalid hardware deck, connect instruments in deck script", "error")
|
|
170
197
|
return redirect(url_for("control.deck_controllers"))
|
|
171
198
|
global_config.deck = module
|
|
172
|
-
global_config.
|
|
199
|
+
global_config.deck_snapshot = module_sigs
|
|
173
200
|
|
|
174
201
|
if script.deck is None:
|
|
175
202
|
script.deck = module.__name__
|
|
@@ -183,47 +210,59 @@ def import_deck():
|
|
|
183
210
|
def save_order(instrument):
|
|
184
211
|
# Save the new order for the specified group to session
|
|
185
212
|
data = request.json
|
|
186
|
-
|
|
187
|
-
card_order[instrument] = data['order']
|
|
188
|
-
session['card_order'] = card_order
|
|
213
|
+
post_session_by_instrument('card_order', instrument, data['order'])
|
|
189
214
|
return '', 204
|
|
190
215
|
|
|
191
216
|
|
|
192
217
|
@control.route('/hide_function/<instrument>/<function>')
|
|
193
218
|
def hide_function(instrument, function):
|
|
194
219
|
back = request.referrer
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
card_order = session.get("card_order")
|
|
198
|
-
order = card_order.get(instrument)
|
|
220
|
+
functions = get_session_by_instrument("hidden_functions", instrument)
|
|
221
|
+
order = get_session_by_instrument("card_order", instrument)
|
|
199
222
|
if function not in functions:
|
|
200
223
|
functions.append(function)
|
|
201
224
|
order.remove(function)
|
|
202
|
-
hidden_functions
|
|
203
|
-
card_order
|
|
204
|
-
session['hidden_functions'] = hidden_functions
|
|
205
|
-
session['card_order'] = card_order
|
|
225
|
+
post_session_by_instrument('hidden_functions', instrument, functions)
|
|
226
|
+
post_session_by_instrument('card_order', instrument, order)
|
|
206
227
|
return redirect(back)
|
|
207
228
|
|
|
208
229
|
|
|
209
230
|
@control.route('/remove_hidden/<instrument>/<function>')
|
|
210
231
|
def remove_hidden(instrument, function):
|
|
211
232
|
back = request.referrer
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
card_order = session.get("card_order")
|
|
215
|
-
order = card_order.get(instrument)
|
|
233
|
+
functions = get_session_by_instrument("hidden_functions", instrument)
|
|
234
|
+
order = get_session_by_instrument("card_order", instrument)
|
|
216
235
|
if function in functions:
|
|
217
236
|
functions.remove(function)
|
|
218
237
|
order.append(function)
|
|
219
|
-
hidden_functions
|
|
220
|
-
card_order
|
|
221
|
-
session['hidden_functions'] = hidden_functions
|
|
222
|
-
session['card_order'] = card_order
|
|
238
|
+
post_session_by_instrument('hidden_functions', instrument, functions)
|
|
239
|
+
post_session_by_instrument('card_order', instrument, order)
|
|
223
240
|
return redirect(back)
|
|
224
241
|
|
|
225
242
|
|
|
243
|
+
def get_session_by_instrument(session_name, instrument):
|
|
244
|
+
"""get data from session by instrument"""
|
|
245
|
+
session_object = session.get(session_name, {})
|
|
246
|
+
functions = session_object.get(instrument, [])
|
|
247
|
+
return functions
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def post_session_by_instrument(session_name, instrument, data):
|
|
251
|
+
"""
|
|
252
|
+
save new data to session by instrument
|
|
253
|
+
:param session_name: "card_order" or "hidden_functions"
|
|
254
|
+
:param instrument: function name of class object
|
|
255
|
+
:param data: order list or hidden function list
|
|
256
|
+
"""
|
|
257
|
+
session_object = session.get(session_name, {})
|
|
258
|
+
session_object[instrument] = data
|
|
259
|
+
session[session_name] = session_object
|
|
260
|
+
|
|
261
|
+
|
|
226
262
|
def find_instrument_by_name(name: str):
|
|
263
|
+
"""
|
|
264
|
+
find instrument class object by instance name
|
|
265
|
+
"""
|
|
227
266
|
if name.startswith("deck"):
|
|
228
267
|
name = name.replace("deck.", "")
|
|
229
268
|
return getattr(global_config.deck, name)
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
</div>
|
|
10
10
|
<h1>{{instrument}} controller</h1>
|
|
11
|
+
{% set hidden = session.get('hidden_functions', {}) %}
|
|
11
12
|
<div class="grid-container" id="sortable-grid">
|
|
12
13
|
{% for function, form in forms.items() %}
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
{% set hidden_instrument = hidden.get(instrument, []) %}
|
|
16
|
+
{% if function not in hidden_instrument %}
|
|
14
17
|
<div class="card" id="{{function}}">
|
|
15
18
|
<div class="bg-white rounded shadow-sm flex-fill">
|
|
16
19
|
<a style="float: right" aria-label="Close" href="{{ url_for('control.hide_function', instrument=instrument, function=function) }}"><i class="bi bi-eye-slash-fill"></i></a>
|
|
@@ -53,7 +56,8 @@
|
|
|
53
56
|
</div>
|
|
54
57
|
<div id="hidden" class="accordion-collapse collapse" data-bs-parent="#accordionActions">
|
|
55
58
|
<div class="accordion-body">
|
|
56
|
-
{%
|
|
59
|
+
{% set hidden_instrument = hidden.get(instrument, []) %}
|
|
60
|
+
{% for function in hidden_instrument %}
|
|
57
61
|
<div>
|
|
58
62
|
{{ function }} <a href="{{ url_for('control.remove_hidden', instrument=instrument, function=function) }}"><i class="bi bi-eye-fill"></i></a>
|
|
59
63
|
</div>
|
|
@@ -61,8 +61,9 @@ def publish():
|
|
|
61
61
|
def finalize():
|
|
62
62
|
script = get_script_file()
|
|
63
63
|
script.finalize()
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
if script.name:
|
|
65
|
+
db.session.merge(script)
|
|
66
|
+
db.session.commit()
|
|
66
67
|
post_script_file(script)
|
|
67
68
|
return redirect(url_for('design.experiment_builder'))
|
|
68
69
|
|
ivoryos/routes/design/design.py
CHANGED
|
@@ -69,7 +69,7 @@ def experiment_builder(instrument=None):
|
|
|
69
69
|
|
|
70
70
|
functions = []
|
|
71
71
|
if deck:
|
|
72
|
-
deck_variables = global_config.
|
|
72
|
+
deck_variables = global_config.deck_snapshot.keys()
|
|
73
73
|
else:
|
|
74
74
|
deck_variables = list(pseudo_deck.keys()) if pseudo_deck else []
|
|
75
75
|
deck_variables.remove("deck_name") if len(deck_variables) > 0 else deck_variables
|
|
@@ -78,7 +78,7 @@ def experiment_builder(instrument=None):
|
|
|
78
78
|
forms = create_builtin_form(instrument)
|
|
79
79
|
else:
|
|
80
80
|
if deck:
|
|
81
|
-
function_metadata = global_config.
|
|
81
|
+
function_metadata = global_config.deck_snapshot.get(instrument, {})
|
|
82
82
|
elif pseudo_deck:
|
|
83
83
|
function_metadata = pseudo_deck.get(instrument, {})
|
|
84
84
|
functions = {key: data.get('signature', {}) for key, data in function_metadata.items()}
|
|
@@ -158,7 +158,7 @@ def generate_code():
|
|
|
158
158
|
prompt = request.form.get("prompt")
|
|
159
159
|
session['prompt'][instrument] = prompt
|
|
160
160
|
# sdl_module = utils.parse_functions(find_instrument_by_name(f'deck.{instrument}'), doc_string=True)
|
|
161
|
-
sdl_module = global_config.
|
|
161
|
+
sdl_module = global_config.deck_snapshot.get(instrument, {})
|
|
162
162
|
empty_script = Script(author=session.get('user'))
|
|
163
163
|
if enable_llm and agent is None:
|
|
164
164
|
try:
|
|
@@ -241,7 +241,7 @@ def experiment_run():
|
|
|
241
241
|
bo_args = request.form.to_dict()
|
|
242
242
|
# ax_client = utils.ax_initiation(bo_args)
|
|
243
243
|
if "online-config" in request.form:
|
|
244
|
-
config = utils.
|
|
244
|
+
config = utils.web_config_entry_wrapper(request.form.to_dict(), config_list)
|
|
245
245
|
repeat = request.form.get('repeat', None)
|
|
246
246
|
|
|
247
247
|
try:
|
|
@@ -134,9 +134,6 @@
|
|
|
134
134
|
<div class="accordion-body">
|
|
135
135
|
This is project is a work in progress. In case of any bugs or suggestions, reach out to Ivory: ivoryzhang@chem.ubc.ca or create an issue on GitLab:
|
|
136
136
|
<a href="https://gitlab.com/heingroup/ivoryos">https://gitlab.com/heingroup/ivoryos</a>.
|
|
137
|
-
{# <img src="{{url_for('static', filename='gui_annotation/installation_guide.PNG')}}">#}
|
|
138
|
-
|
|
139
|
-
{# <object webui_data="{{ url_for('static', filename='setup-help.pdf')}}" type="application/pdf" style="min-height:80vh;width:100%"></object>#}
|
|
140
137
|
</div>
|
|
141
138
|
</div>
|
|
142
139
|
</div>
|
|
@@ -6,7 +6,16 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
|
6
6
|
socket.on('progress', function(data) {
|
|
7
7
|
var progress = data.progress;
|
|
8
8
|
console.log(progress);
|
|
9
|
-
|
|
9
|
+
// Update the progress bar's width and appearance
|
|
10
|
+
var progressBar = document.getElementById('progress-bar-inner');
|
|
11
|
+
progressBar.style.width = progress + '%';
|
|
12
|
+
progressBar.setAttribute('aria-valuenow', progress);
|
|
13
|
+
|
|
14
|
+
if (progress === 100) {
|
|
15
|
+
// Remove animation and set green color when 100% is reached
|
|
16
|
+
progressBar.classList.remove('progress-bar-animated');
|
|
17
|
+
progressBar.classList.add('bg-success'); // Bootstrap class for green color
|
|
18
|
+
}
|
|
10
19
|
});
|
|
11
20
|
socket.on('log', function(data) {
|
|
12
21
|
var logMessage = data.message;
|
ivoryos/static/logo.webp
ADDED
|
Binary file
|
ivoryos/templates/base.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<div class= "container">
|
|
26
26
|
|
|
27
27
|
<a class="navbar-brand" href="{{ url_for('main.index') }}">
|
|
28
|
-
<img src="{{url_for('static', filename='logo.
|
|
28
|
+
<img src="{{url_for('static', filename='logo.webp')}}" alt="Logo" height="60" class="d-inline-block align-text-bottom">
|
|
29
29
|
</a>
|
|
30
30
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
|
31
31
|
<span class="navbar-toggler-icon"></span>
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
|
|
34
34
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
|
35
35
|
<ul class="navbar-nav mr-auto">
|
|
36
|
+
<li class="nav-item">
|
|
37
|
+
<a class="nav-link" href="{{ url_for('main.index') }}" aria-current="page">Home</a>
|
|
38
|
+
</li>
|
|
36
39
|
<li class="nav-item">
|
|
37
40
|
<a class="nav-link" href="{{ url_for('database.load_from_database') }}" aria-current="page">Library</a>
|
|
38
41
|
</li>
|
ivoryos/utils/db_models.py
CHANGED
|
@@ -88,16 +88,16 @@ class Script(db.Model):
|
|
|
88
88
|
if action['uuid'] == int(uuid):
|
|
89
89
|
return action
|
|
90
90
|
|
|
91
|
-
def
|
|
91
|
+
def _convert_type(self, args, arg_types):
|
|
92
92
|
if type(arg_types) is not list:
|
|
93
93
|
arg_types = [arg_types]
|
|
94
|
-
for
|
|
94
|
+
for arg_type in arg_types:
|
|
95
95
|
try:
|
|
96
|
-
args = eval(
|
|
96
|
+
args = eval(f"{arg_type}('{args}')")
|
|
97
97
|
return
|
|
98
98
|
except Exception:
|
|
99
99
|
pass
|
|
100
|
-
raise TypeError(f"Input type error: cannot convert '{args}' to {
|
|
100
|
+
raise TypeError(f"Input type error: cannot convert '{args}' to {arg_type}.")
|
|
101
101
|
|
|
102
102
|
def update_by_uuid(self, uuid, args, output):
|
|
103
103
|
bool_dict = {"True": True, "False": False}
|
|
@@ -113,7 +113,7 @@ class Script(db.Model):
|
|
|
113
113
|
else:
|
|
114
114
|
if arg in action['arg_types']:
|
|
115
115
|
arg_types = action['arg_types'][arg]
|
|
116
|
-
self.
|
|
116
|
+
self._convert_type(args[arg], arg_types)
|
|
117
117
|
else:
|
|
118
118
|
try:
|
|
119
119
|
args[arg] = eval(args[arg])
|
|
@@ -128,7 +128,7 @@ class Script(db.Model):
|
|
|
128
128
|
else:
|
|
129
129
|
if 'arg_types' in action:
|
|
130
130
|
arg_types = action['arg_types']
|
|
131
|
-
self.
|
|
131
|
+
self._convert_type(args, arg_types)
|
|
132
132
|
|
|
133
133
|
# print(args)
|
|
134
134
|
action['args'] = args
|
|
@@ -337,14 +337,13 @@ class Script(db.Model):
|
|
|
337
337
|
|
|
338
338
|
@staticmethod
|
|
339
339
|
def validate_function_name(name):
|
|
340
|
-
|
|
340
|
+
"""Replace invalid characters with underscores"""
|
|
341
341
|
name = re.sub(r'\W|^(?=\d)', '_', name)
|
|
342
342
|
# Check if it's a Python keyword and adjust if necessary
|
|
343
343
|
if keyword.iskeyword(name):
|
|
344
344
|
name += '_'
|
|
345
345
|
return name
|
|
346
346
|
|
|
347
|
-
|
|
348
347
|
def _generate_function_header(self, run_name, stype):
|
|
349
348
|
"""
|
|
350
349
|
Generate the function header.
|
ivoryos/utils/form.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from wtforms.fields.core import Field
|
|
2
|
-
from wtforms.utils import UnsetValue
|
|
3
2
|
from wtforms.validators import InputRequired
|
|
4
3
|
from wtforms.widgets.core import TextInput
|
|
5
4
|
|
|
@@ -88,7 +87,7 @@ class VariableOrFloatField(Field):
|
|
|
88
87
|
raise ValueError(self.gettext("Not a valid float value.")) from exc
|
|
89
88
|
|
|
90
89
|
|
|
91
|
-
unset_value = UnsetValue()
|
|
90
|
+
# unset_value = UnsetValue()
|
|
92
91
|
|
|
93
92
|
|
|
94
93
|
class VariableOrIntField(Field):
|
|
@@ -256,7 +255,7 @@ def create_form_from_module(sdl_module, autofill: bool, script=None, design=True
|
|
|
256
255
|
attr = getattr(sdl_module, attr_name)
|
|
257
256
|
if inspect.ismethod(attr) and not attr_name.startswith('_'):
|
|
258
257
|
form_class = create_add_form(attr, attr_name, autofill, script, design)
|
|
259
|
-
method_forms[attr_name] = form_class()
|
|
258
|
+
method_forms[format_name(attr_name)] = form_class()
|
|
260
259
|
return method_forms
|
|
261
260
|
|
|
262
261
|
|
ivoryos/utils/global_config.py
CHANGED
|
@@ -11,7 +11,7 @@ class GlobalConfig:
|
|
|
11
11
|
cls._instance._agent = None
|
|
12
12
|
cls._instance._defined_variables = {}
|
|
13
13
|
cls._instance._api_variables = set()
|
|
14
|
-
cls._instance.
|
|
14
|
+
cls._instance._deck_snapshot = {}
|
|
15
15
|
cls._instance._runner = None
|
|
16
16
|
return cls._instance
|
|
17
17
|
|
|
@@ -26,12 +26,12 @@ class GlobalConfig:
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
@property
|
|
29
|
-
def
|
|
30
|
-
return self.
|
|
29
|
+
def deck_snapshot(self):
|
|
30
|
+
return self._deck_snapshot
|
|
31
31
|
|
|
32
|
-
@
|
|
33
|
-
def
|
|
34
|
-
self.
|
|
32
|
+
@deck_snapshot.setter
|
|
33
|
+
def deck_snapshot(self, value):
|
|
34
|
+
self._deck_snapshot = value
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
@property
|
ivoryos/utils/llm_agent.py
CHANGED
ivoryos/utils/script_runner.py
CHANGED
|
@@ -53,6 +53,7 @@ class ScriptRunner:
|
|
|
53
53
|
def _run_with_stop_check(self, script: Script, repeat_count, run_name, logger, socketio, config, bo_args,
|
|
54
54
|
output_path):
|
|
55
55
|
time.sleep(1)
|
|
56
|
+
self._emit_progress(socketio, 1)
|
|
56
57
|
try:
|
|
57
58
|
# Run "prep" section once
|
|
58
59
|
script_dict = script.script_dict
|
|
@@ -75,6 +76,7 @@ class ScriptRunner:
|
|
|
75
76
|
finally:
|
|
76
77
|
with self.lock:
|
|
77
78
|
self.is_running = False # Reset the running flag when done
|
|
79
|
+
self._emit_progress(socketio, 100)
|
|
78
80
|
|
|
79
81
|
def _run_actions(self, actions, section_name="", run_name=None, logger=None):
|
|
80
82
|
logger.info(f'Executing {section_name} steps') if actions else logger.info(f'No {section_name} steps')
|
|
@@ -104,7 +106,7 @@ class ScriptRunner:
|
|
|
104
106
|
break
|
|
105
107
|
logger.info(f'Executing {i + 1} of {len(config)} with kwargs = {kwargs}')
|
|
106
108
|
progress = (i + 1) * 100 / len(config)
|
|
107
|
-
|
|
109
|
+
self._emit_progress(socketio, progress)
|
|
108
110
|
fname = f"{run_name}_script"
|
|
109
111
|
function = self.globals_dict[fname]
|
|
110
112
|
output = function(**kwargs)
|
|
@@ -121,8 +123,8 @@ class ScriptRunner:
|
|
|
121
123
|
logger.info(f'Stopping execution during {run_name}: {i + 1}/{int(repeat_count)}')
|
|
122
124
|
break
|
|
123
125
|
logger.info(f'Executing {run_name} experiment: {i + 1}/{int(repeat_count)}')
|
|
124
|
-
progress = (i + 1) * 100 / int(repeat_count)
|
|
125
|
-
|
|
126
|
+
progress = (i + 1) * 100 / int(repeat_count) - 0.1
|
|
127
|
+
self._emit_progress(socketio, progress)
|
|
126
128
|
if bo_args:
|
|
127
129
|
try:
|
|
128
130
|
parameters, trial_index = ax_client.get_next_trial()
|
|
@@ -156,3 +158,7 @@ class ScriptRunner:
|
|
|
156
158
|
writer.writeheader()
|
|
157
159
|
writer.writerows(output_list)
|
|
158
160
|
logger.info(f'Results saved to {file_path}')
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _emit_progress(socketio, progress):
|
|
164
|
+
socketio.emit('progress', {'progress': progress})
|
ivoryos/utils/utils.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ast
|
|
1
2
|
import importlib
|
|
2
3
|
import inspect
|
|
3
4
|
import logging
|
|
@@ -14,6 +15,7 @@ from ivoryos.utils.db_models import Script
|
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def get_script_file():
|
|
18
|
+
"""Get script from Flask session and returns the script"""
|
|
17
19
|
session_script = session.get("scripts")
|
|
18
20
|
if session_script:
|
|
19
21
|
s = Script()
|
|
@@ -24,6 +26,11 @@ def get_script_file():
|
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
def post_script_file(script, is_dict=False):
|
|
29
|
+
"""
|
|
30
|
+
Post script to Flask. Script will be converted to a dict if it is a Script object
|
|
31
|
+
:param script: Script to post
|
|
32
|
+
:param is_dict: if the script is a dictionary,
|
|
33
|
+
"""
|
|
27
34
|
if is_dict:
|
|
28
35
|
session['scripts'] = script
|
|
29
36
|
else:
|
|
@@ -31,12 +38,19 @@ def post_script_file(script, is_dict=False):
|
|
|
31
38
|
|
|
32
39
|
|
|
33
40
|
def create_gui_dir(parent_path):
|
|
41
|
+
"""
|
|
42
|
+
Creates folders for ivoryos data
|
|
43
|
+
"""
|
|
34
44
|
os.makedirs(parent_path, exist_ok=True)
|
|
35
45
|
for path in ["config_csv", "scripts", "results", "pseudo_deck"]:
|
|
36
46
|
os.makedirs(os.path.join(parent_path, path), exist_ok=True)
|
|
37
47
|
|
|
38
48
|
|
|
39
49
|
def save_to_history(filepath, history_path):
|
|
50
|
+
"""
|
|
51
|
+
For manual deck connection only
|
|
52
|
+
save deck file path that successfully connected to ivoryos to a history file
|
|
53
|
+
"""
|
|
40
54
|
connections = []
|
|
41
55
|
try:
|
|
42
56
|
with open(history_path, 'r') as file:
|
|
@@ -50,6 +64,10 @@ def save_to_history(filepath, history_path):
|
|
|
50
64
|
|
|
51
65
|
|
|
52
66
|
def import_history(history_path):
|
|
67
|
+
"""
|
|
68
|
+
For manual deck connection only
|
|
69
|
+
load deck connection history from history file
|
|
70
|
+
"""
|
|
53
71
|
connections = []
|
|
54
72
|
try:
|
|
55
73
|
with open(history_path, 'r') as file:
|
|
@@ -62,11 +80,20 @@ def import_history(history_path):
|
|
|
62
80
|
|
|
63
81
|
|
|
64
82
|
def available_pseudo_deck(path):
|
|
83
|
+
"""
|
|
84
|
+
load pseudo deck (snapshot) from connection history
|
|
85
|
+
"""
|
|
65
86
|
import os
|
|
66
87
|
return os.listdir(path)
|
|
67
88
|
|
|
68
89
|
|
|
69
|
-
def
|
|
90
|
+
def _inspect_class(class_object=None, debug=False):
|
|
91
|
+
"""
|
|
92
|
+
inspect class object: inspect function signature if not name.startswith("_")
|
|
93
|
+
:param class_object: class object
|
|
94
|
+
:param debug: debug mode will inspect function.startswith("_")
|
|
95
|
+
:return: function: Dict[str, Dict[str, Union[Signature, str, None]]]
|
|
96
|
+
"""
|
|
70
97
|
functions = {}
|
|
71
98
|
under_score = "_"
|
|
72
99
|
if debug:
|
|
@@ -79,7 +106,7 @@ def parse_functions(class_object=None, debug=False):
|
|
|
79
106
|
docstring = inspect.getdoc(method)
|
|
80
107
|
functions[function] = dict(signature=annotation, docstring=docstring)
|
|
81
108
|
|
|
82
|
-
# handle getter setters
|
|
109
|
+
# handle getter setters todo
|
|
83
110
|
# if callable(att):
|
|
84
111
|
# functions[function] = inspect.signature(att)
|
|
85
112
|
# else:
|
|
@@ -95,25 +122,25 @@ def parse_functions(class_object=None, debug=False):
|
|
|
95
122
|
|
|
96
123
|
|
|
97
124
|
def _get_type_from_parameters(arg, parameters):
|
|
125
|
+
"""get argument types from inspection"""
|
|
98
126
|
arg_type = ''
|
|
99
127
|
if type(parameters) is inspect.Signature:
|
|
100
|
-
|
|
128
|
+
annotation = parameters.parameters[arg].annotation
|
|
129
|
+
elif type(parameters) is dict:
|
|
130
|
+
annotation = parameters[arg]
|
|
131
|
+
if annotation is not inspect._empty:
|
|
101
132
|
# print(p[arg].annotation)
|
|
102
|
-
if
|
|
103
|
-
|
|
104
|
-
if p[arg].annotation.__module__ == 'typing':
|
|
133
|
+
if annotation.__module__ == 'typing':
|
|
134
|
+
if hasattr(annotation, '_name') and annotation._name in ["Optional", "Union"]:
|
|
105
135
|
# print(p[arg].annotation.__args__)
|
|
106
|
-
arg_type = [i.__name__ for i in
|
|
107
|
-
|
|
108
|
-
arg_type =
|
|
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__]
|
|
136
|
+
arg_type = [i.__name__ for i in annotation.__args__]
|
|
137
|
+
elif hasattr(annotation, '__origin__'):
|
|
138
|
+
arg_type = annotation.__origin__.__name__
|
|
115
139
|
else:
|
|
116
|
-
|
|
140
|
+
# TODO
|
|
141
|
+
pass
|
|
142
|
+
else:
|
|
143
|
+
arg_type = annotation.__name__
|
|
117
144
|
return arg_type
|
|
118
145
|
|
|
119
146
|
|
|
@@ -135,24 +162,24 @@ def find_variable_in_script(script: Script, args: Dict[str, str]) -> Optional[Tu
|
|
|
135
162
|
|
|
136
163
|
|
|
137
164
|
def _convert_by_str(args, arg_types):
|
|
138
|
-
|
|
165
|
+
"""
|
|
166
|
+
Converts a value to type through eval(f'{type}("{args}")')
|
|
167
|
+
"""
|
|
139
168
|
if type(arg_types) is not list:
|
|
140
169
|
arg_types = [arg_types]
|
|
141
|
-
for
|
|
142
|
-
if
|
|
170
|
+
for arg_type in arg_types:
|
|
171
|
+
if not arg_type == "any":
|
|
143
172
|
try:
|
|
144
|
-
args = eval(args)
|
|
173
|
+
args = eval(f'{arg_type}("{args}")') if type(args) is str else eval(f'{arg_type}({args})')
|
|
174
|
+
return args
|
|
145
175
|
except Exception:
|
|
146
|
-
|
|
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}.")
|
|
176
|
+
raise TypeError(f"Input type error: cannot convert '{args}' to {arg_type}.")
|
|
153
177
|
|
|
154
178
|
|
|
155
179
|
def _convert_by_class(args, arg_types):
|
|
180
|
+
"""
|
|
181
|
+
Converts a value to type through type(arg)
|
|
182
|
+
"""
|
|
156
183
|
if arg_types.__module__ == 'builtins':
|
|
157
184
|
args = arg_types(args)
|
|
158
185
|
return args
|
|
@@ -170,6 +197,9 @@ def _convert_by_class(args, arg_types):
|
|
|
170
197
|
|
|
171
198
|
|
|
172
199
|
def convert_config_type(args, arg_types, is_class: bool = False):
|
|
200
|
+
"""
|
|
201
|
+
Converts an argument from str to an arg type
|
|
202
|
+
"""
|
|
173
203
|
bool_dict = {"True": True, "False": False}
|
|
174
204
|
# print(args, arg_types)
|
|
175
205
|
# print(globals())
|
|
@@ -179,20 +209,29 @@ def convert_config_type(args, arg_types, is_class: bool = False):
|
|
|
179
209
|
raise ValueError("config file format not supported.")
|
|
180
210
|
if args[arg] == '' or args[arg] == "None":
|
|
181
211
|
args[arg] = None
|
|
182
|
-
elif args[arg] == "True" or args[arg] == "False":
|
|
183
|
-
|
|
212
|
+
# elif args[arg] == "True" or args[arg] == "False":
|
|
213
|
+
# args[arg] = bool_dict[args[arg]]
|
|
184
214
|
else:
|
|
185
215
|
arg_type = arg_types[arg]
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
216
|
+
try:
|
|
217
|
+
args[arg] = ast.literal_eval(args[arg])
|
|
218
|
+
except ValueError:
|
|
219
|
+
pass
|
|
220
|
+
if type(args[arg]) is not arg_type and not type(args[arg]).__name__ == arg_type:
|
|
221
|
+
if is_class:
|
|
222
|
+
# if arg_type.__module__ == 'builtins':
|
|
223
|
+
args[arg] = _convert_by_class(args[arg], arg_type)
|
|
224
|
+
else:
|
|
225
|
+
args[arg] = _convert_by_str(args[arg], arg_type)
|
|
192
226
|
return args
|
|
193
227
|
|
|
194
228
|
|
|
195
229
|
def import_module_by_filepath(filepath: str, name: str):
|
|
230
|
+
"""
|
|
231
|
+
Import module by file path
|
|
232
|
+
:param filepath: full path of module
|
|
233
|
+
:param name: module's name
|
|
234
|
+
"""
|
|
196
235
|
spec = importlib.util.spec_from_file_location(name, filepath)
|
|
197
236
|
module = importlib.util.module_from_spec(spec)
|
|
198
237
|
spec.loader.exec_module(module)
|
|
@@ -212,6 +251,9 @@ class SocketIOHandler(logging.Handler):
|
|
|
212
251
|
|
|
213
252
|
|
|
214
253
|
def start_logger(socketio: SocketIO, logger_name: str, log_filename: str = None):
|
|
254
|
+
"""
|
|
255
|
+
stream logger to web through web socketIO
|
|
256
|
+
"""
|
|
215
257
|
# logging.basicConfig( format='%(asctime)s - %(message)s')
|
|
216
258
|
formatter = logging.Formatter(fmt='%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
|
217
259
|
logger = logging.getLogger(logger_name)
|
|
@@ -226,7 +268,26 @@ def start_logger(socketio: SocketIO, logger_name: str, log_filename: str = None)
|
|
|
226
268
|
return logger
|
|
227
269
|
|
|
228
270
|
|
|
229
|
-
def ax_wrapper(data):
|
|
271
|
+
def ax_wrapper(data: dict):
|
|
272
|
+
"""
|
|
273
|
+
Ax platform wrapper function for creating optimization campaign parameters and objective from the web form input
|
|
274
|
+
:param data: e.g.,
|
|
275
|
+
{
|
|
276
|
+
"param_1_type": "range", "param_1_value": [1,2],
|
|
277
|
+
"param_2_type": "range", "param_2_value": [1,2],
|
|
278
|
+
"obj_1_min": True,
|
|
279
|
+
"obj_2_min": True
|
|
280
|
+
}
|
|
281
|
+
:return: the optimization campaign parameters
|
|
282
|
+
parameter=[
|
|
283
|
+
{"name": "param_1", "type": "range", "bounds": [1,2]},
|
|
284
|
+
{"name": "param_1", "type": "range", "bounds": [1,2]}
|
|
285
|
+
]
|
|
286
|
+
objectives=[
|
|
287
|
+
{"name": "obj_1", "min": True, "threshold": None},
|
|
288
|
+
{"name": "obj_2", "min": True, "threshold": None},
|
|
289
|
+
]
|
|
290
|
+
"""
|
|
230
291
|
from ax.service.utils.instantiation import ObjectiveProperties
|
|
231
292
|
parameter = []
|
|
232
293
|
objectives = {}
|
|
@@ -259,6 +320,10 @@ def ax_wrapper(data):
|
|
|
259
320
|
|
|
260
321
|
|
|
261
322
|
def ax_initiation(data):
|
|
323
|
+
"""
|
|
324
|
+
create Ax campaign from the web form input
|
|
325
|
+
:param data:
|
|
326
|
+
"""
|
|
262
327
|
install_and_import("ax", "ax-platform")
|
|
263
328
|
parameter, objectives = ax_wrapper(data)
|
|
264
329
|
from ax.service.ax_client import AxClient
|
|
@@ -277,6 +342,11 @@ def get_arg_type(args, parameters):
|
|
|
277
342
|
|
|
278
343
|
|
|
279
344
|
def install_and_import(package, package_name=None):
|
|
345
|
+
"""
|
|
346
|
+
Install the package and import it
|
|
347
|
+
:param package: package to import and install
|
|
348
|
+
:param package_name: pip install package name if different from package
|
|
349
|
+
"""
|
|
280
350
|
try:
|
|
281
351
|
# Check if the package is already installed
|
|
282
352
|
importlib.import_module(package)
|
|
@@ -288,7 +358,12 @@ def install_and_import(package, package_name=None):
|
|
|
288
358
|
# print(f"{package} has been installed successfully.")
|
|
289
359
|
|
|
290
360
|
|
|
291
|
-
def
|
|
361
|
+
def web_config_entry_wrapper(data: dict, config_type: list):
|
|
362
|
+
"""
|
|
363
|
+
Wrap the data dictionary from web config entries during execution configuration
|
|
364
|
+
:param data: data dictionary
|
|
365
|
+
:param config_type: data entry types ["str", "int", "float", "bool"]
|
|
366
|
+
"""
|
|
292
367
|
rows = {} # Dictionary to hold webui_data organized by rows
|
|
293
368
|
|
|
294
369
|
# Organize webui_data by rows
|
|
@@ -311,22 +386,32 @@ def process_data(data, config_type):
|
|
|
311
386
|
return filtered_rows
|
|
312
387
|
|
|
313
388
|
|
|
314
|
-
def
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
389
|
+
def create_deck_snapshot(deck, save: bool = False, output_path: str = ''):
|
|
390
|
+
"""
|
|
391
|
+
Create a deck snapshot of the given script
|
|
392
|
+
:param deck: python module name to create the deck snapshot from e.g. __main__
|
|
393
|
+
:param save: save the deck snapshot into pickle file
|
|
394
|
+
:param output_path: path to save the pickle file
|
|
395
|
+
"""
|
|
396
|
+
deck_snapshot = {f"deck.{name}": _inspect_class(val) for name, val in vars(deck).items()
|
|
397
|
+
if not type(val).__module__ == 'builtins'
|
|
398
|
+
and not name[0].isupper()
|
|
399
|
+
and not name.startswith("_")}
|
|
400
|
+
if deck_snapshot and save:
|
|
320
401
|
# pseudo_deck = parse_dict
|
|
321
|
-
parse_dict =
|
|
402
|
+
parse_dict = deck_snapshot.copy()
|
|
322
403
|
parse_dict["deck_name"] = os.path.splitext(os.path.basename(deck.__file__))[
|
|
323
404
|
0] if deck.__name__ == "__main__" else deck.__name__
|
|
324
405
|
with open(os.path.join(output_path, f"{parse_dict['deck_name']}.pkl"), 'wb') as file:
|
|
325
406
|
pickle.dump(parse_dict, file)
|
|
326
|
-
return
|
|
407
|
+
return deck_snapshot
|
|
327
408
|
|
|
328
409
|
|
|
329
|
-
def load_deck(pkl_name):
|
|
410
|
+
def load_deck(pkl_name: str):
|
|
411
|
+
"""
|
|
412
|
+
Loads a pickled deck snapshot from disk on offline mode
|
|
413
|
+
:param pkl_name: name of the pickle file
|
|
414
|
+
"""
|
|
330
415
|
if not pkl_name:
|
|
331
416
|
return None
|
|
332
417
|
try:
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ivoryos
|
|
3
|
+
Version: 0.1.7
|
|
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
|
+
"plug and play" web UI extension for flexible SDLs.
|
|
23
|
+
|
|
24
|
+
## Table of Contents
|
|
25
|
+
- [Description](#description)
|
|
26
|
+
- [System requirements](#system-requirements)
|
|
27
|
+
- [Installation](#installation)
|
|
28
|
+
- [Instructions for use](#instructions-for-use)
|
|
29
|
+
- [Demo](#demo)
|
|
30
|
+
- [License](#license)
|
|
31
|
+
|
|
32
|
+
## Description
|
|
33
|
+
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).
|
|
34
|
+
This web UI aims to ease up the control of any Python-based SDLs by displaying functions and parameters for initialized modules dynamically.
|
|
35
|
+
The modules can be hardware API, high-level functions, or experiment workflow.
|
|
36
|
+
With the least modification of the current workflow, user can design, manage and execute their experimental designs and monitor the execution process.
|
|
37
|
+
|
|
38
|
+
## System requirements
|
|
39
|
+
This software is developed and tested using Windows. This software and its dependencies are compatible across major platforms: Linux, macOS, and Windows. Some dependencies (Flask-SQLAlchemy) may require additional setup.
|
|
40
|
+
|
|
41
|
+
### Python Version
|
|
42
|
+
Python >=3.7 for best compatibility.
|
|
43
|
+
### Python dependencies
|
|
44
|
+
This software is compatible with the latest versions of all dependencies.
|
|
45
|
+
- bcrypt~=4.0
|
|
46
|
+
- Flask-Login~=0.6
|
|
47
|
+
- Flask-Session~=0.8
|
|
48
|
+
- Flask-SocketIO~=5.3
|
|
49
|
+
- Flask-SQLAlchemy~=3.1
|
|
50
|
+
- SQLAlchemy-Utils~=0.41
|
|
51
|
+
- Flask-WTF~=1.2
|
|
52
|
+
- python-dotenv==1.0.1
|
|
53
|
+
- openai (optional ~=1.53)
|
|
54
|
+
- ax-platform (optional ~=0.3 or ~=0.4 for Python>=3.9)
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
```bash
|
|
58
|
+
pip install ivoryos
|
|
59
|
+
```
|
|
60
|
+
or
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://gitlab.com/heingroup/ivoryos.git
|
|
63
|
+
cd ivoryos
|
|
64
|
+
pip install -e .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The installation may take 10 to 30 seconds to install. The installation time may vary and take up to several minutes, depending on the network speed, computer performance, and virtual environment settings.
|
|
68
|
+
|
|
69
|
+
## Instructions for use
|
|
70
|
+
### Quick start
|
|
71
|
+
In your SDL script, use `ivoryos(__name__)`.
|
|
72
|
+
```python
|
|
73
|
+
import ivoryos
|
|
74
|
+
|
|
75
|
+
ivoryos.run(__name__)
|
|
76
|
+
```
|
|
77
|
+
### Login
|
|
78
|
+
Create an account and login (local database)
|
|
79
|
+
### Features
|
|
80
|
+
- **Direct control**: direct function calling _Device_ tab
|
|
81
|
+
- **Workflow design and iteration**:
|
|
82
|
+
- **Design**: add function to canvas in _Design_ tab. click `Compile and Run` button to go to the execution page
|
|
83
|
+
- **Execution**: configure iteration methods and parameters in _Compile/Run_ tab.
|
|
84
|
+
- **Database**: manage workflows in _Library_ tab.
|
|
85
|
+
- **Info page**: additional info in _About_ tab.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
### Additional settings
|
|
89
|
+
#### AI assistant
|
|
90
|
+
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.
|
|
91
|
+
|
|
92
|
+
#### Enable LLMs with [OpenAI API](https://github.com/openai/openai-python)
|
|
93
|
+
1. Create a `.env` file for `OPENAI_API_KEY`
|
|
94
|
+
```
|
|
95
|
+
OPENAI_API_KEY="Your API Key"
|
|
96
|
+
```
|
|
97
|
+
2. In your SDL script, define model, you can use any GPT models.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
ivoryos.run(__name__, model="gpt-3.5-turbo")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Enable local LLMs with [Ollama](https://ollama.com/)
|
|
104
|
+
1. Download Ollama.
|
|
105
|
+
2. pull models from Ollama
|
|
106
|
+
3. In your SDL script, define LLM server and model, you can use any models available on Ollama.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
ivoryos.run(__name__, llm_server="localhost", model="llama3.1")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### Add additional logger(s)
|
|
113
|
+
```python
|
|
114
|
+
ivoryos.run(__name__, logger="logger name")
|
|
115
|
+
```
|
|
116
|
+
or
|
|
117
|
+
```python
|
|
118
|
+
ivoryos.run(__name__, logger=["logger 1", "logger 2"])
|
|
119
|
+
```
|
|
120
|
+
#### Offline (design without hardware connection)
|
|
121
|
+
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.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
ivoryos.run()
|
|
125
|
+
```
|
|
126
|
+
## Demo
|
|
127
|
+
In the [abstract_sdl.py](https://gitlab.com/heingroup/ivoryos/-/blob/main/example/sdl_example/abstract_sdl.py), where instances of `AbstractSDL` is created as `sdl`,
|
|
128
|
+
addresses will be available on terminal.
|
|
129
|
+
```Python
|
|
130
|
+
ivoryos.run(__name__)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
* Running on all addresses (0.0.0.0)
|
|
134
|
+
* Running on http://127.0.0.1:8000
|
|
135
|
+
* Running on http://xxx.xx.xx.xxx:8000
|
|
136
|
+
|
|
137
|
+
### Deck function and web form
|
|
138
|
+

|
|
139
|
+
|
|
140
|
+
### Directory structure
|
|
141
|
+
|
|
142
|
+
When you run the application for the first time, it will automatically create the following folders and files in the same directory:
|
|
143
|
+
|
|
144
|
+
- **`ivoryos_data/`**: Main directory for application-related data.
|
|
145
|
+
- **`ivoryos_data/config_csv/`**: Contains iteration configuration files in CSV format.
|
|
146
|
+
- **`ivoryos_data/llm_output/`**: Stores raw prompt generated for the large language model.
|
|
147
|
+
- **`ivoryos_data/pseudo_deck/`**: Contains pseudo-deck `.pkl` files for offline access.
|
|
148
|
+
- **`ivoryos_data/results/`**: Used for storing results or outputs during workflow execution.
|
|
149
|
+
- **`ivoryos_data/scripts/`**: Holds Python scripts compiled from the visual programming script design.
|
|
150
|
+
|
|
151
|
+
- **`default.log`**: Log file that captures application logs.
|
|
152
|
+
- **`ivoryos.db`**: Database file that stores application data locally.
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
### Demo video
|
|
156
|
+
Intro + Tutorial + Demo with PurPOSE platform
|
|
157
|
+
https://youtu.be/dFfJv9I2-1g
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
## Authors and Acknowledgement
|
|
161
|
+
Ivory Zhang, Lucy Hao
|
|
162
|
+
|
|
163
|
+
Authors acknowledge all former and current Hein Lab members for their valuable suggestions.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
[LICENSE](LICENSE)
|
|
@@ -1,46 +1,47 @@
|
|
|
1
|
-
ivoryos/__init__.py,sha256=
|
|
2
|
-
ivoryos/config.py,sha256=
|
|
1
|
+
ivoryos/__init__.py,sha256=HuYhPOMFGkQ_f9s1dZXaknAylKXds0jYK0nmdf1I6Z8,3932
|
|
2
|
+
ivoryos/config.py,sha256=K03jdGmbUfJ9o4kK6NOtDGJtydGHFq8-oU8nvCyq5zQ,1358
|
|
3
3
|
ivoryos/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
ivoryos/routes/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
ivoryos/routes/auth/auth.py,sha256=NWII2gQEUHxQmuwd4QKRrGczKVo6uWMuPq24P9TUUss,2381
|
|
6
6
|
ivoryos/routes/auth/templates/auth/login.html,sha256=1uxYU7NpxVaA4sfwkC6CuzZXJdy1VnxBzztmsMPcrxE,1232
|
|
7
7
|
ivoryos/routes/auth/templates/auth/signup.html,sha256=QQ7n4OBnF8TNFS5_4s11n4BCqSePn429rZfA6vO8qw8,1497
|
|
8
8
|
ivoryos/routes/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
ivoryos/routes/control/control.py,sha256=
|
|
10
|
-
ivoryos/routes/control/templates/control/controllers.html,sha256=
|
|
9
|
+
ivoryos/routes/control/control.py,sha256=YKM1T_bDIggbW_NF6Ld6-rb3d0KbAjyikJztN-ka_XM,11305
|
|
10
|
+
ivoryos/routes/control/templates/control/controllers.html,sha256=CD1DEm9DuBBlKg_ltrEWgNxWhTIR9C2_a0-AkxwGre0,4146
|
|
11
11
|
ivoryos/routes/control/templates/control/controllers_home.html,sha256=IND1T3_mPZd-MzfuyodbedMnmsTowiTVdRp5ez6NoZM,2783
|
|
12
12
|
ivoryos/routes/control/templates/control/controllers_new.html,sha256=Wqn9x9D6K7RWHkLFxvZkzbIJxHJR1zywQ6WDgySXOig,5010
|
|
13
13
|
ivoryos/routes/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
ivoryos/routes/database/database.py,sha256=
|
|
14
|
+
ivoryos/routes/database/database.py,sha256=e1OpmQayM0KYUTPiYQzXHo-zLVd5yHtS0EYNce7vcmQ,4357
|
|
15
15
|
ivoryos/routes/database/templates/database/experiment_database.html,sha256=x9zf4u4KbG6BEICnH_TOVNNUkp5oAmGBB12OUX0PPl4,3506
|
|
16
16
|
ivoryos/routes/design/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
ivoryos/routes/design/design.py,sha256=
|
|
17
|
+
ivoryos/routes/design/design.py,sha256=FUuO-1iSeKhrLQ5Qz4zQwZiFRC5rax7RwhJBH9Wt42U,16895
|
|
18
18
|
ivoryos/routes/design/templates/design/experiment_builder.html,sha256=IAU9XvtbcEWDlnCqf2Z81gcBB7bNDFOfod3xLJfRbQc,27371
|
|
19
19
|
ivoryos/routes/design/templates/design/experiment_run.html,sha256=xoEHH8CC83KCWTPavwP9JWUI8SE5HX9bkEfJN6vMg5s,22845
|
|
20
20
|
ivoryos/routes/main/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
ivoryos/routes/main/main.py,sha256=Zkagtw0E67FaspNJ87jkYpc-wqqoTq99dyyB7qdnOVM,655
|
|
22
|
-
ivoryos/routes/main/templates/main/help.html,sha256=
|
|
22
|
+
ivoryos/routes/main/templates/main/help.html,sha256=TY6uD-v8MXpiZlGucchko1Bas5jHRzARIgCgneBZ_ok,9511
|
|
23
23
|
ivoryos/routes/main/templates/main/home.html,sha256=fjeSSXkuVDr2y-CQmskNHf1yYFFdrPPI4wn6_XPb6AY,3252
|
|
24
24
|
ivoryos/static/favicon.ico,sha256=RhlrPtfITOkzC9BjP1UB1V5L9Oyp6NwNtWeMcGOnpyc,15406
|
|
25
25
|
ivoryos/static/logo.png,sha256=7lNyToDllflGPUK2sj7IBR8FkHLC-6gi-OVSL9o4jrs,63464
|
|
26
|
+
ivoryos/static/logo.webp,sha256=lXgfQR-4mHTH83k7VV9iB54-oC2ipe6uZvbwdOnLETc,14974
|
|
26
27
|
ivoryos/static/style.css,sha256=rY6n2mbP_xNavtVin_yUqtcvNm6uqAF82t7ONE2Sx9E,3918
|
|
27
28
|
ivoryos/static/gui_annotation/Slide1.png,sha256=Lm4gdOkUF5HIUFaB94tl6koQVkzpitKj43GXV_XYMMc,121727
|
|
28
29
|
ivoryos/static/gui_annotation/Slide2.PNG,sha256=z3wQ9oVgg4JTWVLQGKK_KhtepRHUYP1e05XUWGT2A0I,118761
|
|
29
30
|
ivoryos/static/js/overlay.js,sha256=44l9THVKYZfa7HX6siyqY7EdFWKBk5pyyKgN0_7ZnrM,495
|
|
30
|
-
ivoryos/static/js/socket_handler.js,sha256=
|
|
31
|
+
ivoryos/static/js/socket_handler.js,sha256=2936CldW6Po_saWh1aL_EV-VydJVIvikrNfTaSfU1sE,1449
|
|
31
32
|
ivoryos/static/js/sortable_card.js,sha256=mDVd2YjhusLokUw3xL6YOZLXIzty9yKDsC1U5yR8aC8,831
|
|
32
33
|
ivoryos/static/js/sortable_design.js,sha256=BxNXzqET_yY0xpS1Fc0iwPCnkkDwYMiuVqkgOPMb6JY,1156
|
|
33
|
-
ivoryos/templates/base.html,sha256=
|
|
34
|
+
ivoryos/templates/base.html,sha256=KcKMjITaaC23yzIRN535uMhzv5x96nJl245Y2GaqdsM,7943
|
|
34
35
|
ivoryos/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
-
ivoryos/utils/db_models.py,sha256=
|
|
36
|
-
ivoryos/utils/form.py,sha256=
|
|
37
|
-
ivoryos/utils/global_config.py,sha256=
|
|
38
|
-
ivoryos/utils/llm_agent.py,sha256=
|
|
39
|
-
ivoryos/utils/script_runner.py,sha256=
|
|
36
|
+
ivoryos/utils/db_models.py,sha256=umEATdxDP-sR_AMaLrOW2PBUcmBf1w38DLeFkBvVjJE,19280
|
|
37
|
+
ivoryos/utils/form.py,sha256=mqNDuaxniaGnKcJ26N9dcXpHQ6TabXTqPVbcrcDvM4s,12239
|
|
38
|
+
ivoryos/utils/global_config.py,sha256=JCQvmZB0pNC-EjveRu48Tp4uvcNwn9DP3Ema6Xd9fJY,1656
|
|
39
|
+
ivoryos/utils/llm_agent.py,sha256=z0DIpZzc-z09p-diUZIOE5L9zfFW8RwseFjbfUvEqoQ,6596
|
|
40
|
+
ivoryos/utils/script_runner.py,sha256=gtqiHy4-40j5FMERXrmGb4jb9RAPzjCR345PMPduDno,7120
|
|
40
41
|
ivoryos/utils/task_manager.py,sha256=xfQ1s9ywWDrCYYpdgliVvoWED0s2xARmo3LXvAy6fgY,2517
|
|
41
|
-
ivoryos/utils/utils.py,sha256=
|
|
42
|
-
ivoryos-0.1.
|
|
43
|
-
ivoryos-0.1.
|
|
44
|
-
ivoryos-0.1.
|
|
45
|
-
ivoryos-0.1.
|
|
46
|
-
ivoryos-0.1.
|
|
42
|
+
ivoryos/utils/utils.py,sha256=BMmvyBNo8PYs-MiBiiHjYPvSwrHORofbNwhPYpaVnfI,15249
|
|
43
|
+
ivoryos-0.1.7.dist-info/LICENSE,sha256=psyqat4GJkzi42551i0kH-bXLbEzrQEnjPDwy2TVhv8,1105
|
|
44
|
+
ivoryos-0.1.7.dist-info/METADATA,sha256=SgV11aX0JSQjJRsn_1vDlt15DrdAEWKM7DSs5dU6cjE,6101
|
|
45
|
+
ivoryos-0.1.7.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
|
46
|
+
ivoryos-0.1.7.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
|
|
47
|
+
ivoryos-0.1.7.dist-info/RECORD,,
|
ivoryos-0.1.5.dist-info/METADATA
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|