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 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
- app = Flask(__name__)
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.deck_variables = utils.parse_deck(global_config.deck, output_path=app.config["DUMMY_DECK"], save=True)
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.deck_variables.keys()
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
- card_order = session.get('card_order')
81
- order = card_order.get(instrument, _forms.keys())
82
- if instrument not in card_order:
83
- card_order[instrument] = list(order)
84
- session['card_order'] = card_order
85
- # print(session['card_order'])
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.parse_deck(module, save=update, output_path=current_app.config["DUMMY_DECK"])
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.deck_variables = module_sigs
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
- card_order = session.get("card_order", {})
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
- hidden_functions = session.get("hidden_functions")
196
- functions = hidden_functions.get(instrument, [])
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[instrument] = functions
203
- card_order[instrument] = 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
- hidden_functions = session.get("hidden_functions")
213
- functions = hidden_functions.get(instrument, [])
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[instrument] = functions
220
- card_order[instrument] = 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
- {% if function not in session['hidden_functions'][instrument] %}
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
- {% for function in session['hidden_functions'][instrument] %}
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
- db.session.merge(script)
65
- db.session.commit()
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
 
@@ -69,7 +69,7 @@ def experiment_builder(instrument=None):
69
69
 
70
70
  functions = []
71
71
  if deck:
72
- deck_variables = global_config.deck_variables.keys()
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.deck_variables.get(instrument, {})
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.deck_variables.get(instrument, {})
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.process_data(request.form.to_dict(), config_list)
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
- $('#progress-bar-inner').css('width', progress + '%')
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;
Binary file
@@ -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.png')}}" alt="Logo" height="60" class="d-inline-block align-text-bottom">
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>
@@ -88,16 +88,16 @@ class Script(db.Model):
88
88
  if action['uuid'] == int(uuid):
89
89
  return action
90
90
 
91
- def _convert(self, args, arg_types):
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 i in arg_types:
94
+ for arg_type in arg_types:
95
95
  try:
96
- args = eval(i + "('" + args + "')")
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 {i}.")
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._convert(args[arg], arg_types)
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._convert(args, arg_types)
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
- # Replace invalid characters with underscores
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
 
@@ -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._deck_variables = {}
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 deck_variables(self):
30
- return self._deck_variables
29
+ def deck_snapshot(self):
30
+ return self._deck_snapshot
31
31
 
32
- @deck_variables.setter
33
- def deck_variables(self, value):
34
- self._deck_variables = value
32
+ @deck_snapshot.setter
33
+ def deck_snapshot(self, value):
34
+ self._deck_snapshot = value
35
35
 
36
36
 
37
37
  @property
@@ -3,7 +3,7 @@ import json
3
3
  import os
4
4
  import re
5
5
 
6
- from openai import OpenAI, BaseModel
6
+ from openai import OpenAI
7
7
 
8
8
 
9
9
  # from dotenv import load_dotenv
@@ -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
- socketio.emit('progress', {'progress': progress})
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
- socketio.emit('progress', {'progress': progress})
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 parse_functions(class_object=None, debug=False):
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
- p = parameters.parameters
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 p[arg].annotation is not inspect._empty:
103
- # print(p[arg].annotation)
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 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__]
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
- arg_type = parameters[arg].__name__
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
- # print(arg_types)
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 i in arg_types:
142
- if i == "any":
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
- 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}.")
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
- args[arg] = bool_dict[args[arg]]
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
- 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)
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 process_data(data, config_type):
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 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:
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 = deck_variables.copy()
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 deck_variables
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
+ ![](https://gitlab.com/heingroup/ivoryos/raw/main/docs/ivoryos.png)
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
+ ![](https://gitlab.com/heingroup/ivoryos/raw/main/docs/demo.gif)
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=XCKzSohAH86YPczmErM4njggMrnUsRBxsG8Uj7CKLyk,3526
2
- ivoryos/config.py,sha256=wi0sDjr4E-TImj85BQbKcT-wY3TlZbcGVTpJda7twvE,1297
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=xicTfpMo2hF6AVz2ZC9Y8qLXqGH8vyqPsKzrCZZ1BQ0,9607
10
- ivoryos/routes/control/templates/control/controllers.html,sha256=nPQ7JmM0KmPV8bh1VMdZmyfyGEEb7fb4nsThdk9ZaoY,3984
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=HF5JyUHtpR9L6IBBVCnyNTDgc1OH6XnVc2Y5RgXIejo,4328
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=AOSbz7a-TZWkh_EUNCSXF-D_XjMjLKfmLYHwncYvW9s,16886
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=RMHCDMKn0AyKfADNF7iex1CYid_KJQP7CtpervIM2NM,9786
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=HduGphUyyZq8GE1-5ZmDgHGTzcWDRa6oXBNrWWMnxwM,989
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=cu6MZMlfOR0IhuNrMin_pWYtm1_TpJLHI6KH5NomopI,7763
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=VXkOSnYVABoYgCTYo1F9BH-Zd922ToMuvwyRaFtG7cc,19248
36
- ivoryos/utils/form.py,sha256=2dFtuhwcu5g_fksXd-FH414z5y_htTfHB0btfPxD7b0,12262
37
- ivoryos/utils/global_config.py,sha256=oCegHTk2k9rGrODNFrnwrgFaww-5QUp0dGdO5sz-GO8,1662
38
- ivoryos/utils/llm_agent.py,sha256=pxiaiIVHzbGA_Tq4yOZY3iyIDikx6-_chIvPEjlywLg,6607
39
- ivoryos/utils/script_runner.py,sha256=jlJMCFLmcS6GOlOfEmLXAzG47sU18nNtRPtojjdN018,6919
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=Oia2ZITlW-oRglmjc_Y02FP-dwHeEFB_nrZ-4bNHkVY,12060
42
- ivoryos-0.1.5.dist-info/LICENSE,sha256=psyqat4GJkzi42551i0kH-bXLbEzrQEnjPDwy2TVhv8,1105
43
- ivoryos-0.1.5.dist-info/METADATA,sha256=ljumv9yicCP_tMkfeFl5P7yTGw4WhYPqdDFQSL2yb8g,3585
44
- ivoryos-0.1.5.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
45
- ivoryos-0.1.5.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
46
- ivoryos-0.1.5.dist-info/RECORD,,
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,,
@@ -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
- ![](https://gitlab.com/heingroup/ivoryos/raw/main/docs/ivoryos.png)
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
- ![](https://gitlab.com/heingroup/ivoryos/raw/main/docs/demo.gif)
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)