ivoryos 0.1.25__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ivoryos might be problematic. Click here for more details.

@@ -6,8 +6,10 @@ from flask_login import login_required
6
6
  from ivoryos.utils.global_config import GlobalConfig
7
7
  from ivoryos.utils import utils
8
8
  from ivoryos.utils.form import create_form_from_module, format_name
9
+ from ivoryos.utils.task_runner import TaskRunner
9
10
 
10
11
  global_config = GlobalConfig()
12
+ runner = TaskRunner()
11
13
 
12
14
  control = Blueprint('control', __name__, template_folder='templates/control')
13
15
 
@@ -142,7 +144,9 @@ def controllers(instrument: str):
142
144
  if form and form.validate_on_submit():
143
145
  try:
144
146
  kwargs.pop("hidden_name")
145
- output = function_executable(**kwargs)
147
+ output = runner.run_single_step(instrument, method_name, kwargs, wait=True,
148
+ current_app=current_app._get_current_object())
149
+ # output = function_executable(**kwargs)
146
150
  flash(f"\nRun Success! Output value: {output}.")
147
151
  except Exception as e:
148
152
  flash(e.__str__())
@@ -170,24 +174,14 @@ def backend_control(instrument: str=None):
170
174
  forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
171
175
 
172
176
  if request.method == 'POST':
173
- all_kwargs = request.form.copy()
174
- method_name = all_kwargs.pop("hidden_name", None)
175
- # if method_name is not None:
177
+ method_name = request.form.get("hidden_name", None)
176
178
  form = forms.get(method_name, None)
177
- kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
178
- function_executable = getattr(inst_object, method_name)
179
179
  if form:
180
- # print(kwargs)
181
- try:
182
- kwargs.pop("hidden_name")
183
- output = function_executable(**kwargs)
184
- json_output = jsonify(output)
185
- except Exception as e:
186
- json_output = jsonify(e.__str__())
187
- return json_output, 400
188
- else:
189
- return "instrument not exist", 400
190
- return json_output, 200
180
+ kwargs = {field.name: field.data for field in form if field.name not in ['csrf_token', 'hidden_name']}
181
+ wait = request.form.get("hidden_wait", "true") == "true"
182
+ output = runner.run_single_step(component=instrument, method=method_name, kwargs=kwargs, wait=wait,
183
+ current_app=current_app._get_current_object())
184
+ return jsonify(output), 200
191
185
 
192
186
 
193
187
  @control.route("/backend_control", methods=['GET'])
@@ -29,6 +29,11 @@ def edit_workflow(workflow_name):
29
29
  off_line = current_app.config["OFF_LINE"]
30
30
  if off_line and pseudo_name and not script.deck == pseudo_name:
31
31
  flash(f"Choose the deck with name {script.deck}")
32
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
33
+ return jsonify({
34
+ "script": script.as_dict(),
35
+ "python_script": script.compile(),
36
+ })
32
37
  return redirect(url_for('design.experiment_builder'))
33
38
 
34
39
 
@@ -101,7 +106,7 @@ def finalize():
101
106
  return redirect(url_for('design.experiment_builder'))
102
107
 
103
108
 
104
- @database.route("/database/")
109
+ @database.route("/database/", strict_slashes=False)
105
110
  @database.route("/database/<deck_name>")
106
111
  @login_required
107
112
  def load_from_database(deck_name=None):
@@ -131,7 +136,15 @@ def load_from_database(deck_name=None):
131
136
  per_page = 10
132
137
 
133
138
  workflows = query.paginate(page=page, per_page=per_page, error_out=False)
134
- return render_template("experiment_database.html", workflows=workflows, deck_list=deck_list, deck_name=deck_name)
139
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
140
+ workflows = query.all()
141
+ workflow_names = [w.name for w in workflows]
142
+ return jsonify({
143
+ "workflows": workflow_names,
144
+ })
145
+ else:
146
+ # return HTML
147
+ return render_template("experiment_database.html", workflows=workflows, deck_list=deck_list, deck_name=deck_name)
135
148
 
136
149
 
137
150
  @database.route("/edit_run_name", methods=['POST'])
@@ -200,6 +213,12 @@ def list_workflows():
200
213
  per_page = 10
201
214
 
202
215
  workflows = query.paginate(page=page, per_page=per_page, error_out=False)
216
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
217
+ workflows = query.all()
218
+ workflow_data = {w.id:{"workflow_name":w.name, "start_time":w.start_time} for w in workflows}
219
+ return jsonify({
220
+ "workflow_data": workflow_data,
221
+ })
203
222
  return render_template('workflow_run_database.html', workflows=workflows)
204
223
 
205
224
 
@@ -208,20 +227,40 @@ def get_workflow_steps(workflow_id):
208
227
  workflow = WorkflowRun.query.get_or_404(workflow_id)
209
228
  steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).order_by(WorkflowStep.start_time).all()
210
229
 
211
- # Organize steps by phase + repeat_index
230
+ # Use full objects for template rendering
212
231
  grouped = {
213
232
  "prep": [],
214
233
  "script": {},
215
234
  "cleanup": [],
216
235
  }
217
236
 
237
+ # Use dicts for JSON response
238
+ grouped_json = {
239
+ "prep": [],
240
+ "script": {},
241
+ "cleanup": [],
242
+ }
243
+
218
244
  for step in steps:
245
+ step_dict = step.as_dict()
246
+
219
247
  if step.phase == "prep":
220
248
  grouped["prep"].append(step)
249
+ grouped_json["prep"].append(step_dict)
250
+
221
251
  elif step.phase == "script":
222
252
  grouped["script"].setdefault(step.repeat_index, []).append(step)
253
+ grouped_json["script"].setdefault(step.repeat_index, []).append(step_dict)
254
+
223
255
  elif step.phase == "cleanup" or step.method_name == "stop":
224
256
  grouped["cleanup"].append(step)
257
+ grouped_json["cleanup"].append(step_dict)
258
+
259
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
260
+ return jsonify({
261
+ "workflow_info": workflow.as_dict(),
262
+ "steps": grouped_json,
263
+ })
225
264
 
226
265
  return render_template("experiment_step_view.html", workflow=workflow, grouped=grouped)
227
266
 
@@ -15,7 +15,7 @@ from ivoryos.utils.client_proxy import create_function, export_to_python
15
15
  from ivoryos.utils.global_config import GlobalConfig
16
16
  from ivoryos.utils.form import create_builtin_form, create_action_button, format_name, create_form_from_pseudo, \
17
17
  create_form_from_action, create_all_builtin_forms
18
- from ivoryos.utils.db_models import Script
18
+ from ivoryos.utils.db_models import Script, WorkflowRun, SingleStep, WorkflowStep
19
19
  from ivoryos.utils.script_runner import ScriptRunner
20
20
  # from ivoryos.utils.utils import load_workflows
21
21
 
@@ -25,32 +25,45 @@ design = Blueprint('design', __name__, template_folder='templates/design')
25
25
  global_config = GlobalConfig()
26
26
  runner = ScriptRunner()
27
27
 
28
-
29
- @socketio.on('abort_pending')
30
- def handle_abort_pending():
28
+ def abort_pending():
31
29
  runner.abort_pending()
32
30
  socketio.emit('log', {'message': "aborted pending iterations"})
33
31
 
34
-
35
- @socketio.on('abort_current')
36
- def handle_abort_current():
32
+ def abort_current():
37
33
  runner.stop_execution()
38
34
  socketio.emit('log', {'message': "stopped next task"})
39
35
 
40
-
41
- @socketio.on('pause')
42
- def handle_pause():
36
+ def pause():
43
37
  runner.retry = False
44
38
  msg = runner.toggle_pause()
45
39
  socketio.emit('log', {'message': msg})
40
+ return msg
46
41
 
47
- @socketio.on('retry')
48
- def handle_pause():
42
+ def retry():
49
43
  runner.retry = True
50
44
  msg = runner.toggle_pause()
51
45
  socketio.emit('log', {'message': msg})
52
46
 
53
47
 
48
+ # ---- Socket.IO Event Handlers ----
49
+
50
+ @socketio.on('abort_pending')
51
+ def handle_abort_pending():
52
+ abort_pending()
53
+
54
+ @socketio.on('abort_current')
55
+ def handle_abort_current():
56
+ abort_current()
57
+
58
+ @socketio.on('pause')
59
+ def handle_pause():
60
+ pause()
61
+
62
+ @socketio.on('retry')
63
+ def handle_retry():
64
+ retry()
65
+
66
+
54
67
  @socketio.on('connect')
55
68
  def handle_abort_action():
56
69
  # Fetch log messages from local file
@@ -312,11 +325,18 @@ def experiment_run():
312
325
  config_preview = []
313
326
  config_file_list = [i for i in os.listdir(current_app.config["CSV_FOLDER"]) if not i == ".gitkeep"]
314
327
  try:
315
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
316
- except ValueError as e:
328
+ # todo
329
+ exec_string = script.python_script if script.python_script else script.compile(current_app.config['SCRIPT_FOLDER'])
330
+ # exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
331
+
332
+ except Exception as e:
317
333
  flash(e.__str__())
318
- return redirect(url_for("design.experiment_builder"))
319
- # print(exec_string)
334
+ # handle api request
335
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
336
+ return jsonify({"error": e.__str__()})
337
+ else:
338
+ return redirect(url_for("design.experiment_builder"))
339
+
320
340
  config_file = request.args.get("filename")
321
341
  config = []
322
342
  if config_file:
@@ -356,25 +376,41 @@ def experiment_run():
356
376
  flash(f"This script is not compatible with current deck, import {script.deck}")
357
377
  if request.method == "POST":
358
378
  bo_args = None
359
- if "bo" in request.form:
360
- bo_args = request.form.to_dict()
361
- # ax_client = utils.ax_initiation(bo_args)
362
- if "online-config" in request.form:
363
- config = utils.web_config_entry_wrapper(request.form.to_dict(), config_list)
364
- repeat = request.form.get('repeat', None)
379
+ compiled = False
380
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
381
+ payload_json = request.get_json()
382
+ compiled = True
383
+ if "kwargs" in payload_json:
384
+ config = payload_json["kwargs"]
385
+ elif "parameters" in payload_json:
386
+ bo_args = payload_json
387
+ repeat = payload_json.pop("repeat", None)
388
+ else:
389
+ if "bo" in request.form:
390
+ bo_args = request.form.to_dict()
391
+ if "online-config" in request.form:
392
+ config = utils.web_config_entry_wrapper(request.form.to_dict(), config_list)
393
+ repeat = request.form.get('repeat', None)
365
394
 
366
395
  try:
367
396
  datapath = current_app.config["DATA_FOLDER"]
368
397
  run_name = script.validate_function_name(run_name)
369
398
  runner.run_script(script=script, run_name=run_name, config=config, bo_args=bo_args,
370
399
  logger=g.logger, socketio=g.socketio, repeat_count=repeat,
371
- output_path=datapath, current_app=current_app._get_current_object()
400
+ output_path=datapath, compiled=compiled,
401
+ current_app=current_app._get_current_object()
372
402
  )
373
403
  if utils.check_config_duplicate(config):
374
404
  flash(f"WARNING: Duplicate in config entries.")
375
405
  except Exception as e:
376
- flash(e)
377
- return render_template('experiment_run.html', script=script.script_dict, filename=filename,
406
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
407
+ return jsonify({"error": e.__str__()})
408
+ else:
409
+ flash(e)
410
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
411
+ return jsonify({"status": "task started", "task_id": global_config.runner_status.get("id")})
412
+ else:
413
+ return render_template('experiment_run.html', script=script.script_dict, filename=filename,
378
414
  dot_py=exec_string, line_collection=line_collection,
379
415
  return_list=return_list, config_list=config_list, config_file_list=config_file_list,
380
416
  config_preview=config_preview, data_list=data_list, config_type_list=config_type_list,
@@ -629,6 +665,66 @@ def duplicate_action(id: int):
629
665
  return redirect(back)
630
666
 
631
667
 
632
- @design.route("/backend/status", methods=["GET"])
668
+ # ---- HTTP API Endpoints ----
669
+
670
+ @design.route("/api/status", methods=["GET"])
633
671
  def runner_status():
634
- return jsonify(runner.get_status())
672
+ runner_busy = global_config.runner_lock.locked()
673
+ status = {"busy": runner_busy}
674
+ task_status = global_config.runner_status
675
+ current_step = {}
676
+ # print(task_status)
677
+ if task_status is not None:
678
+ task_type = task_status["type"]
679
+ task_id = task_status["id"]
680
+ if task_type == "task":
681
+ step = SingleStep.query.get(task_id)
682
+ current_step = step.as_dict()
683
+ if task_type == "workflow":
684
+ workflow = WorkflowRun.query.get(task_id)
685
+ if workflow is not None:
686
+ latest_step = WorkflowStep.query.filter_by(workflow_id=workflow.id).order_by(WorkflowStep.start_time.desc()).first()
687
+ if latest_step is not None:
688
+ current_step = latest_step.as_dict()
689
+ status["workflow_status"] = {"workflow_info": workflow.as_dict(), "runner_status": runner.get_status()}
690
+ status["current_task"] = current_step
691
+ return jsonify(status), 200
692
+
693
+
694
+
695
+ @design.route("/api/abort_pending", methods=["POST"])
696
+ def api_abort_pending():
697
+ abort_pending()
698
+ return jsonify({"status": "ok"}), 200
699
+
700
+ @design.route("/api/abort_current", methods=["POST"])
701
+ def api_abort_current():
702
+ abort_current()
703
+ return jsonify({"status": "ok"}), 200
704
+
705
+ @design.route("/api/pause", methods=["POST"])
706
+ def api_pause():
707
+ msg = pause()
708
+ return jsonify({"status": "ok", "pause_status": msg}), 200
709
+
710
+ @design.route("/api/retry", methods=["POST"])
711
+ def api_retry():
712
+ retry()
713
+ return jsonify({"status": "ok, retrying failed step"}), 200
714
+
715
+
716
+ @design.route("/api/get_script", methods=["GET", "POST"])
717
+ def get_script():
718
+ script = utils.get_script_file()
719
+ script.sort_actions()
720
+ script_collection = script.compile()
721
+ if request.method == "POST":
722
+ # create a brand-new script
723
+ deck = global_config.deck
724
+ deck_name = os.path.splitext(os.path.basename(deck.__file__))[0] if deck.__name__ == "__main__" else deck.__name__
725
+ script = Script(author=session.get('user'), deck=deck_name)
726
+ script_collection = request.get_json()
727
+ script.python_script = script_collection
728
+ utils.post_script_file(script)
729
+ return jsonify({"status": "ok"}), 200
730
+ return jsonify(script_collection), 200
@@ -26,8 +26,8 @@
26
26
  </h2>
27
27
  <div id="runpanel" class="accordion-collapse collapse show">
28
28
  <div class="accordion-body">
29
- {% if script['script'] or script['prep'] or script['cleanup'] %}
30
- <div class="row">
29
+ <div class="row">
30
+ {% if script['script'] or script['prep'] or script['cleanup'] %}
31
31
  <div class="col-lg-6 col-sm-12" id="run-panel" style="{{ 'display: none;' if pause_status else '' }}">
32
32
  <ul class="nav nav-tabs" id="myTabs" role="tablist">
33
33
  <li class="nav-item" role="presentation">
@@ -127,129 +127,62 @@
127
127
  </form>
128
128
  {% endif %}
129
129
  </div>
130
-
131
- {# <div class="tab-pane fade " id="tab3" role="tabpanel" aria-labelledby="tab3-tab">#}
132
- {# <form role="form" method='POST' name="bo" action="{{ url_for('design.experiment_run')}}">#}
133
- {# <div class="form-group">#}
134
- {# <p><h5>Parameters:</h5><p>#}
135
- {# {% for config in config_list %}#}
136
- {# <div class="row g-3 align-items-center">#}
137
- {# <div class="col-lg-3 col-sm-6 ">#}
138
- {# {{config}}:#}
139
- {# </div>#}
140
- {# <div class="col-auto">#}
141
- {# <label class="col-form-label" for="{{config}}_type">Type</label>#}
142
- {# </div>#}
143
- {# <div class="col-auto">#}
144
- {# <select class="form-select" id="{{config}}_type" name="{{config}}_type">#}
145
- {# <option selected value="range">range</option>#}
146
- {# <option value="choice">choice</option>#}
147
- {# <option value="fixed">fixed</option>#}
148
- {# </select>#}
149
- {# </div>#}
150
- {# <div class="col-auto">#}
151
- {# <label class="" for="{{config}}_value">Values</label>#}
152
- {# </div>#}
153
- {# <div class="col-auto">#}
154
- {# <input type="text" class="form-control" id="{{config}}_value" name="{{config}}_value" placeholder="1, 2, 3">#}
155
- {# </div>#}
156
- {# <div class="col-auto">#}
157
- {# <input type="text" class="form-control" id="{{config}}_value_max" style="display: none;" placeholder="1, 2, 3">#}
158
- {# </div>#}
159
- {# </div>#}
160
- {# {% endfor %}#}
161
- {# <p><h5>Objective:</h5><p>#}
162
- {# {% for objective in return_list %}#}
163
- {# <div class="row gy-2 gx-3 align-items-center input-group">#}
164
- {# <div class="col-3">#}
165
- {# {{objective}}:#}
166
- {# </div>#}
167
- {# <div class="col-auto">#}
168
- {# <label class="" for="{{objective}}_min">Minimize</label>#}
169
- {# </div>#}
170
- {# <div class="col-auto">#}
171
- {# <select class="form-select" id="{{objective}}_min" name="{{objective}}_min">#}
172
- {# <option selected>minimize</option>#}
173
- {# <option>maximize</option>#}
174
- {# <option>none</option>#}
175
- {# </select>#}
176
- {# </div>#}
177
- {# <div class="col-auto">#}
178
- {# <label class="" for="{{objective}}_threshold">Threshold</label>#}
179
- {# </div>#}
180
- {# <div class="col-auto">#}
181
- {# <input type="text" class="form-control" id="{{objective}}_threshold" name="{{objective}}_threshold" placeholder="None">#}
182
- {# </div>#}
183
- {# </div>#}
184
- {# {% endfor %}#}
185
- {# <p><h5>Budget:</h5></p>#}
186
- {# <div class="input-group mb-3">#}
187
- {# <label class="input-group-text" for="repeat">Max iteration </label>#}
188
- {# <input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="25">#}
189
- {# </div>#}
190
- {# {% if not no_deck_warning%}#}
191
- {# <div class="input-group mb-3">#}
192
- {# <button class="form-control" type="submit" name="bo">Run</button>#}
193
- {# </div>#}
194
- {# {% endif %}#}
195
- {# </div>#}
196
- {# </form>#}
197
- {# </div>#}
198
- <div class="tab-pane fade" id="tab3" role="tabpanel" aria-labelledby="tab3-tab">
199
- <form method="POST" name="bo" action="{{ url_for('design.experiment_run') }}">
200
- <div class="container py-2">
201
130
 
202
- <!-- Parameters -->
203
- <h6 class="fw-bold mt-2 mb-1">Parameters</h6>
204
- {% for config in config_list %}
205
- <div class="row align-items-center mb-2">
206
- <div class="col-3 col-form-label-sm">
207
- {{ config }}:
208
- </div>
209
- <div class="col-6">
210
- <select class="form-select form-select-sm" id="{{config}}_type" name="{{config}}_type">
211
- <option selected value="range">range</option>
212
- <option value="choice">choice</option>
213
- <option value="fixed">fixed</option>
214
- </select>
215
- </div>
216
- <div class="col-3">
217
- <input type="text" class="form-control form-control-sm" id="{{config}}_value" name="{{config}}_value" placeholder="1, 2, 3">
218
- </div>
219
- </div>
220
- {% endfor %}
131
+ <div class="tab-pane fade" id="tab3" role="tabpanel" aria-labelledby="tab3-tab">
132
+ <form method="POST" name="bo" action="{{ url_for('design.experiment_run') }}">
133
+ <div class="container py-2">
221
134
 
222
- <!-- Objective -->
223
- <h6 class="fw-bold mt-3 mb-1">Objectives</h6>
224
- {% for objective in return_list %}
225
- <div class="row align-items-center mb-2">
226
- <div class="col-3 col-form-label-sm">
227
- {{ objective }}:
228
- </div>
229
- <div class="col-6">
230
- <select class="form-select form-select-sm" id="{{objective}}_min" name="{{objective}}_min">
231
- <option selected>minimize</option>
232
- <option>maximize</option>
233
- <option>none</option>
234
- </select>
235
- </div>
236
- </div>
237
- {% endfor %}
135
+ <!-- Parameters -->
136
+ <h6 class="fw-bold mt-2 mb-1">Parameters</h6>
137
+ {% for config in config_list %}
138
+ <div class="row align-items-center mb-2">
139
+ <div class="col-3 col-form-label-sm">
140
+ {{ config }}:
141
+ </div>
142
+ <div class="col-6">
143
+ <select class="form-select form-select-sm" id="{{config}}_type" name="{{config}}_type">
144
+ <option selected value="range">range</option>
145
+ <option value="choice">choice</option>
146
+ <option value="fixed">fixed</option>
147
+ </select>
148
+ </div>
149
+ <div class="col-3">
150
+ <input type="text" class="form-control form-control-sm" id="{{config}}_value" name="{{config}}_value" placeholder="1, 2, 3">
151
+ </div>
152
+ </div>
153
+ {% endfor %}
238
154
 
239
- <h6 class="fw-bold mt-3 mb-1">Budget</h6>
155
+ <!-- Objective -->
156
+ <h6 class="fw-bold mt-3 mb-1">Objectives</h6>
157
+ {% for objective in return_list %}
158
+ <div class="row align-items-center mb-2">
159
+ <div class="col-3 col-form-label-sm">
160
+ {{ objective }}:
161
+ </div>
162
+ <div class="col-6">
163
+ <select class="form-select form-select-sm" id="{{objective}}_min" name="{{objective}}_min">
164
+ <option selected>minimize</option>
165
+ <option>maximize</option>
166
+ <option>none</option>
167
+ </select>
168
+ </div>
169
+ </div>
170
+ {% endfor %}
240
171
 
241
- <div class="input-group mb-3">
242
- <label class="input-group-text" for="repeat">Max iteration </label>
243
- <input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="25">
244
- </div>
245
- {% if not no_deck_warning%}
246
- <div class="input-group mb-3">
247
- <button class="form-control" type="submit" name="bo">Run</button>
248
- </div>
249
- {% endif %}
250
- </div>
251
- </form>
252
- </div>
172
+ <h6 class="fw-bold mt-3 mb-1">Budget</h6>
173
+
174
+ <div class="input-group mb-3">
175
+ <label class="input-group-text" for="repeat">Max iteration </label>
176
+ <input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="25">
177
+ </div>
178
+ {% if not no_deck_warning%}
179
+ <div class="input-group mb-3">
180
+ <button class="form-control" type="submit" name="bo">Run</button>
181
+ </div>
182
+ {% endif %}
183
+ </div>
184
+ </form>
185
+ </div>
253
186
 
254
187
 
255
188
  <div class="tab-pane fade {{ 'show active' if config_list and config_list|count<=5 else '' }}" id="tab4" role="tabpanel" aria-labelledby="tab4-tab">
@@ -278,6 +211,12 @@
278
211
  </div>
279
212
  </div>
280
213
  </div>
214
+ {% else %}
215
+ <div class="col-lg-6 col-sm-12" id="placeholder-panel">
216
+
217
+ </div>
218
+ {% endif %}
219
+
281
220
  <div class="col-lg-6 col-sm-12" id="code-panel" style="{{ '' if pause_status else 'display: none;'}}">
282
221
  <p>
283
222
  <h5>Progress:</h5>
@@ -299,7 +238,7 @@
299
238
  {% endif %}
300
239
  {% if "cleanup" in line_collection.keys() %}
301
240
  {% set stype = "cleanup" %}
302
- <h6>Experiment:</h6>
241
+ <h6>Cleanup:</h6>
303
242
  {% for code in line_collection["cleanup"] %}
304
243
  <pre style="margin: 0; padding: 0; line-height: 1;"><code class="python" id="{{ stype }}-{{ loop.index0 }}" >{{code}}</code></pre>
305
244
  {% endfor %}
@@ -337,7 +276,7 @@
337
276
  <div id="logging-panel"></div>
338
277
  </div>
339
278
  </div>
340
- {% endif %}
279
+
341
280
  </div>
342
281
  </div>
343
282
  </div>
@@ -10,21 +10,26 @@ document.addEventListener("DOMContentLoaded", function() {
10
10
  var progressBar = document.getElementById('progress-bar-inner');
11
11
  progressBar.style.width = progress + '%';
12
12
  progressBar.setAttribute('aria-valuenow', progress);
13
+ const runPanel = document.getElementById("run-panel");
14
+ const codePanel = document.getElementById("code-panel");
13
15
  if (progress === 1) {
14
- document.getElementById("run-panel").style.display = "none";
15
- document.getElementById("code-panel").style.display = "block";
16
-
17
- // Optional: Scroll to the code panel
18
- document.getElementById("code-panel").scrollIntoView({ behavior: "smooth" });
16
+ if (runPanel) runPanel.style.display = "none";
17
+ if (codePanel) {
18
+ codePanel.style.display = "block";
19
+ codePanel.scrollIntoView({ behavior: "smooth" });
20
+ }
21
+ progressBar.classList.remove('bg-success');
22
+ progressBar.classList.remove('bg-danger');
23
+ progressBar.classList.add('progress-bar-animated');
19
24
  }
20
25
  if (progress === 100) {
21
26
  // Remove animation and set green color when 100% is reached
22
27
  progressBar.classList.remove('progress-bar-animated');
23
28
  progressBar.classList.add('bg-success'); // Bootstrap class for green color
24
29
  setTimeout(() => {
25
- document.getElementById("code-panel").style.display = "none";
26
- document.getElementById("run-panel").style.display = "block";
27
- }, 1000); // Small delay to let users see the completion
30
+ if (runPanel) runPanel.style.display = "block";
31
+ if (codePanel) codePanel.style.display = "none";
32
+ }, 1000); // Small delay to let users see the completion
28
33
  }
29
34
  });
30
35
 
@@ -0,0 +1,87 @@
1
+ from ivoryos.utils.utils import install_and_import
2
+
3
+
4
+ def ax_init_form(data, arg_types):
5
+ """
6
+ create Ax campaign from the web form input
7
+ :param data:
8
+ """
9
+ install_and_import("ax", "ax-platform")
10
+ parameter, objectives = ax_wrapper(data, arg_types)
11
+ from ax.service.ax_client import AxClient
12
+ ax_client = AxClient()
13
+ ax_client.create_experiment(parameter, objectives=objectives)
14
+ return ax_client
15
+
16
+
17
+ def ax_wrapper(data: dict, arg_types: list):
18
+ """
19
+ Ax platform wrapper function for creating optimization campaign parameters and objective from the web form input
20
+ :param data: e.g.,
21
+ {
22
+ "param_1_type": "range", "param_1_value": [1,2],
23
+ "param_2_type": "range", "param_2_value": [1,2],
24
+ "obj_1_min": True,
25
+ "obj_2_min": True
26
+ }
27
+ :return: the optimization campaign parameters
28
+ parameter=[
29
+ {"name": "param_1", "type": "range", "bounds": [1,2]},
30
+ {"name": "param_1", "type": "range", "bounds": [1,2]}
31
+ ]
32
+ objectives=[
33
+ {"name": "obj_1", "min": True, "threshold": None},
34
+ {"name": "obj_2", "min": True, "threshold": None},
35
+ ]
36
+ """
37
+ from ax.service.utils.instantiation import ObjectiveProperties
38
+ parameter = []
39
+ objectives = {}
40
+ # Iterate through the webui_data dictionary
41
+ for key, value in data.items():
42
+ # Check if the key corresponds to a parameter type
43
+ if "_type" in key:
44
+ param_name = key.split("_type")[0]
45
+ param_type = value
46
+ param_value = data[f"{param_name}_value"].split(",")
47
+ try:
48
+ values = [float(v) for v in param_value]
49
+ except Exception:
50
+ values = param_value
51
+ if param_type == "range":
52
+ param = {"name": param_name, "type": param_type, "bounds": values}
53
+ if param_type == "choice":
54
+ param = {"name": param_name, "type": param_type, "values": values}
55
+ if param_type == "fixed":
56
+ param = {"name": param_name, "type": param_type, "value": values[0]}
57
+ _type = arg_types[param_name] if arg_types[param_name] in ["str", "bool", "int"] else "float"
58
+ param.update({"value_type": _type})
59
+ parameter.append(param)
60
+ elif key.endswith("_min"):
61
+ if not value == 'none':
62
+ obj_name = key.split("_min")[0]
63
+ is_min = True if value == "minimize" else False
64
+
65
+ threshold = None if f"{obj_name}_threshold" not in data else data[f"{obj_name}_threshold"]
66
+ properties = ObjectiveProperties(minimize=is_min)
67
+ objectives[obj_name] = properties
68
+
69
+ return parameter, objectives
70
+
71
+
72
+ def ax_init_opc(bo_args):
73
+ install_and_import("ax", "ax-platform")
74
+ from ax.service.ax_client import AxClient
75
+ from ax.service.utils.instantiation import ObjectiveProperties
76
+
77
+ ax_client = AxClient()
78
+ objectives = bo_args.get("objectives")
79
+ objectives_formatted = {}
80
+ for obj in objectives:
81
+ obj_name = obj.get("name")
82
+ minimize = obj.get("minimize")
83
+ objectives_formatted[obj_name] = ObjectiveProperties(minimize=minimize)
84
+ bo_args["objectives"] = objectives_formatted
85
+ ax_client.create_experiment(**bo_args)
86
+
87
+ return ax_client
@@ -48,7 +48,8 @@ class Script(db.Model):
48
48
 
49
49
  def __init__(self, name=None, deck=None, status=None, script_dict: dict = None, id_order: dict = None,
50
50
  time_created=None, last_modified=None, editing_type=None, author: str = None,
51
- registered:bool=False,
51
+ # registered:bool=False,
52
+ python_script: str = None
52
53
  ):
53
54
  if script_dict is None:
54
55
  script_dict = {"prep": [], "script": [], "cleanup": []}
@@ -76,6 +77,7 @@ class Script(db.Model):
76
77
  self.id_order = id_order
77
78
  self.editing_type = editing_type
78
79
  self.author = author
80
+ self.python_script = python_script
79
81
  # self.r = registered
80
82
 
81
83
  def as_dict(self):
@@ -412,12 +414,13 @@ class Script(db.Model):
412
414
  """
413
415
  line_collection = {}
414
416
  for stype, func_str in exec_str_collection.items():
415
- module = ast.parse(func_str)
416
- func_def = next(node for node in module.body if isinstance(node, ast.FunctionDef))
417
+ if func_str:
418
+ module = ast.parse(func_str)
419
+ func_def = next(node for node in module.body if isinstance(node, ast.FunctionDef))
417
420
 
418
- # Extract function body as source lines
419
- line_collection[stype] = [ast.unparse(node) for node in func_def.body if not isinstance(node, ast.Return)]
420
- # print(line_collection[stype])
421
+ # Extract function body as source lines
422
+ line_collection[stype] = [ast.unparse(node) for node in func_def.body if not isinstance(node, ast.Return)]
423
+ # print(line_collection[stype])
421
424
  return line_collection
422
425
 
423
426
  def compile(self, script_path=None):
@@ -670,7 +673,24 @@ class WorkflowStep(db.Model):
670
673
  run_error = db.Column(db.Boolean, default=False)
671
674
 
672
675
  def as_dict(self):
673
- dict = self.__dict__
676
+ dict = self.__dict__.copy()
677
+ dict.pop('_sa_instance_state', None)
678
+ return dict
679
+
680
+
681
+ class SingleStep(db.Model):
682
+ __tablename__ = 'single_steps'
683
+
684
+ id = db.Column(db.Integer, primary_key=True)
685
+ method_name = db.Column(db.String(128), nullable=False)
686
+ kwargs = db.Column(JSONType, nullable=False)
687
+ start_time = db.Column(db.DateTime)
688
+ end_time = db.Column(db.DateTime)
689
+ run_error = db.Column(db.String(128))
690
+ output = db.Column(JSONType)
691
+
692
+ def as_dict(self):
693
+ dict = self.__dict__.copy()
674
694
  dict.pop('_sa_instance_state', None)
675
695
  return dict
676
696
 
@@ -1,4 +1,4 @@
1
- # from ivoryos.utils.script_runner import ScriptRunner
1
+ import threading
2
2
 
3
3
 
4
4
  class GlobalConfig:
@@ -13,7 +13,8 @@ class GlobalConfig:
13
13
  cls._instance._defined_variables = {}
14
14
  cls._instance._api_variables = set()
15
15
  cls._instance._deck_snapshot = {}
16
- cls._instance._runner = None
16
+ cls._instance._runner_lock = threading.Lock()
17
+ cls._instance._runner_status = None
17
18
  return cls._instance
18
19
 
19
20
  @property
@@ -70,9 +71,17 @@ class GlobalConfig:
70
71
  self._api_variables = value
71
72
 
72
73
  @property
73
- def runner(self):
74
- return self._runner
74
+ def runner_lock(self):
75
+ return self._runner_lock
75
76
 
76
- @runner.setter
77
- def runner(self, value):
78
- self._runner = value
77
+ @runner_lock.setter
78
+ def runner_lock(self, value):
79
+ self._runner_lock = value
80
+
81
+ @property
82
+ def runner_status(self):
83
+ return self._runner_status
84
+
85
+ @runner_status.setter
86
+ def runner_status(self, value):
87
+ self._runner_status = value
@@ -5,8 +5,8 @@ import threading
5
5
  import time
6
6
  from datetime import datetime
7
7
 
8
- from ivoryos.utils import utils
9
- from ivoryos.utils.db_models import Script, WorkflowRun, WorkflowStep, db
8
+ from ivoryos.utils import utils, bo_campaign
9
+ from ivoryos.utils.db_models import Script, WorkflowRun, WorkflowStep, db, SingleStep
10
10
  from ivoryos.utils.global_config import GlobalConfig
11
11
 
12
12
  global_config = GlobalConfig()
@@ -26,8 +26,9 @@ class ScriptRunner:
26
26
  self.stop_pending_event = threading.Event()
27
27
  self.stop_current_event = threading.Event()
28
28
  self.is_running = False
29
- self.lock = threading.Lock()
29
+ self.lock = global_config.runner_lock
30
30
  self.paused = False
31
+ self.current_app = None
31
32
 
32
33
  def toggle_pause(self):
33
34
  """Toggles between pausing and resuming the script"""
@@ -59,23 +60,29 @@ class ScriptRunner:
59
60
  self.stop_current_event.set()
60
61
  self.abort_pending()
61
62
 
63
+
62
64
  def run_script(self, script, repeat_count=1, run_name=None, logger=None, socketio=None, config=None, bo_args=None,
63
- output_path="", current_app=None):
64
- # Get run.id
65
+ output_path="", compiled=False, current_app=None):
65
66
  global deck
66
67
  if deck is None:
67
68
  deck = global_config.deck
68
- time.sleep(1)
69
- with self.lock:
70
- if self.is_running:
69
+
70
+ if self.current_app is None:
71
+ self.current_app = current_app
72
+ time.sleep(1) # Optional: may help ensure deck readiness
73
+
74
+ # Try to acquire lock without blocking
75
+ if not self.lock.acquire(blocking=False):
76
+ if logger:
71
77
  logger.info("System is busy. Please wait for it to finish or stop it before starting a new one.")
72
- return None
73
- self.is_running = True
78
+ return None
74
79
 
75
80
  self.reset_stop_event()
76
81
 
77
- thread = threading.Thread(target=self._run_with_stop_check,
78
- args=(script, repeat_count, run_name, logger, socketio, config, bo_args, output_path, current_app))
82
+ thread = threading.Thread(
83
+ target=self._run_with_stop_check,
84
+ args=(script, repeat_count, run_name, logger, socketio, config, bo_args, output_path, current_app, compiled)
85
+ )
79
86
  thread.start()
80
87
  return thread
81
88
 
@@ -86,7 +93,7 @@ class ScriptRunner:
86
93
  :param kwargs: Arguments to pass to the function
87
94
  :return: The final result of the function execution
88
95
  """
89
- _func_str = script.compile()
96
+ _func_str = script.python_script or script.compile()
90
97
  step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
91
98
  global deck
92
99
  # global deck, registered_workflows
@@ -138,6 +145,8 @@ class ScriptRunner:
138
145
  method_name=method_name,
139
146
  start_time=start_time,
140
147
  )
148
+ db.session.add(step)
149
+ db.session.commit()
141
150
  logger.info(f"Executing: {line}")
142
151
  socketio.emit('execution', {'section': f"{section_name}-{index}"})
143
152
  # self._emit_progress(socketio, 100)
@@ -158,7 +167,7 @@ class ScriptRunner:
158
167
  step.run_error = True
159
168
  self.toggle_pause()
160
169
  step.end_time = datetime.now()
161
- db.session.add(step)
170
+ # db.session.add(step)
162
171
  db.session.commit()
163
172
 
164
173
  self.pause_event.wait()
@@ -173,7 +182,7 @@ class ScriptRunner:
173
182
  return exec_locals # Return the 'results' variable
174
183
 
175
184
  def _run_with_stop_check(self, script: Script, repeat_count: int, run_name: str, logger, socketio, config, bo_args,
176
- output_path, current_app):
185
+ output_path, current_app, compiled):
177
186
  time.sleep(1)
178
187
  # _func_str = script.compile()
179
188
  # step_list_dict: dict = script.convert_to_lines(_func_str)
@@ -183,11 +192,12 @@ class ScriptRunner:
183
192
  script_dict = script.script_dict
184
193
  with current_app.app_context():
185
194
 
186
- run = WorkflowRun(name=script.name or "untitled", platform=script.deck,start_time=datetime.now())
195
+ run = WorkflowRun(name=script.name or "untitled", platform=script.deck or "deck",start_time=datetime.now())
187
196
  db.session.add(run)
188
- db.session.flush()
189
-
190
- self._run_actions(script, section_name="prep", logger=logger, socketio=socketio, run_id=run.id)
197
+ db.session.commit()
198
+ run_id = run.id # Save the ID
199
+ global_config.runner_status = {"id":run_id, "type": "workflow"}
200
+ self._run_actions(script, section_name="prep", logger=logger, socketio=socketio, run_id=run_id)
191
201
  output_list = []
192
202
  _, arg_type = script.config("script")
193
203
  _, return_list = script.config_return()
@@ -195,43 +205,45 @@ class ScriptRunner:
195
205
  # Run "script" section multiple times
196
206
  if repeat_count:
197
207
  self._run_repeat_section(repeat_count, arg_type, bo_args, output_list, script,
198
- run_name, return_list, logger, socketio, run_id=run.id)
208
+ run_name, return_list, compiled, logger, socketio, run_id=run_id)
199
209
  elif config:
200
210
  self._run_config_section(config, arg_type, output_list, script, run_name, logger,
201
- socketio, run_id=run.id)
211
+ socketio, run_id=run_id, compiled=compiled)
202
212
 
203
213
  # Run "cleanup" section once
204
- self._run_actions(script, section_name="cleanup", logger=logger, socketio=socketio,run_id=run.id)
214
+ self._run_actions(script, section_name="cleanup", logger=logger, socketio=socketio,run_id=run_id)
205
215
  # Reset the running flag when done
206
- with self.lock:
207
- self.is_running = False
216
+ self.lock.release()
208
217
  # Save results if necessary
209
218
  filename = None
210
- if output_list:
219
+ if not script.python_script and output_list:
211
220
  filename = self._save_results(run_name, arg_type, return_list, output_list, logger, output_path)
212
221
  self._emit_progress(socketio, 100)
222
+ with current_app.app_context():
223
+ run = db.session.get(WorkflowRun, run_id) # SQLAlchemy 1.4+ recommended method
213
224
  run.end_time = datetime.now()
214
225
  run.data_path = filename
215
226
  db.session.commit()
216
227
 
217
228
  def _run_actions(self, script, section_name="", logger=None, socketio=None, run_id=None):
218
- _func_str = script.compile()
229
+ _func_str = script.python_script or script.compile()
219
230
  step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
220
231
  logger.info(f'Executing {section_name} steps') if step_list else logger.info(f'No {section_name} steps')
221
232
  if self.stop_pending_event.is_set():
222
233
  logger.info(f"Stopping execution during {section_name} section.")
223
234
  return
224
- self.exec_steps(script, section_name, logger, socketio, run_id=run_id, i_progress=0)
235
+ if step_list:
236
+ self.exec_steps(script, section_name, logger, socketio, run_id=run_id, i_progress=0)
225
237
 
226
- def _run_config_section(self, config, arg_type, output_list, script, run_name, logger, socketio, run_id):
227
- compiled = True
228
- for i in config:
229
- try:
230
- i = utils.convert_config_type(i, arg_type)
231
- except Exception as e:
232
- logger.info(e)
233
- compiled = False
234
- break
238
+ def _run_config_section(self, config, arg_type, output_list, script, run_name, logger, socketio, run_id, compiled=True):
239
+ if not compiled:
240
+ for i in config:
241
+ try:
242
+ i = utils.convert_config_type(i, arg_type)
243
+ except Exception as e:
244
+ logger.info(e)
245
+ compiled = False
246
+ break
235
247
  if compiled:
236
248
  for i, kwargs in enumerate(config):
237
249
  kwargs = dict(kwargs)
@@ -248,11 +260,14 @@ class ScriptRunner:
248
260
  # kwargs.update(output)
249
261
  output_list.append(output)
250
262
 
251
- def _run_repeat_section(self, repeat_count, arg_types, bo_args, output_list, script, run_name, return_list,
263
+ def _run_repeat_section(self, repeat_count, arg_types, bo_args, output_list, script, run_name, return_list, compiled,
252
264
  logger, socketio, run_id):
253
265
  if bo_args:
254
266
  logger.info('Initializing optimizer...')
255
- ax_client = utils.ax_initiation(bo_args, arg_types)
267
+ if compiled:
268
+ ax_client = bo_campaign.ax_init_opc(bo_args)
269
+ else:
270
+ ax_client = bo_campaign.ax_init_form(bo_args, arg_types)
256
271
  for i_progress in range(int(repeat_count)):
257
272
  if self.stop_pending_event.is_set():
258
273
  logger.info(f'Stopping execution during {run_name}: {i_progress + 1}/{int(repeat_count)}')
@@ -311,9 +326,9 @@ class ScriptRunner:
311
326
 
312
327
  def get_status(self):
313
328
  """Returns current status of the script runner."""
314
- with self.lock:
329
+ with self.current_app.app_context():
315
330
  return {
316
- "is_running": self.is_running,
331
+ "is_running": self.lock.locked(),
317
332
  "paused": self.paused,
318
333
  "stop_pending": self.stop_pending_event.is_set(),
319
334
  "stop_current": self.stop_current_event.is_set(),
@@ -0,0 +1,81 @@
1
+ import threading
2
+ import time
3
+ from datetime import datetime
4
+
5
+ from ivoryos.utils.db_models import db, SingleStep
6
+ from ivoryos.utils.global_config import GlobalConfig
7
+
8
+ global_config = GlobalConfig()
9
+ global deck
10
+ deck = None
11
+
12
+
13
+ class TaskRunner:
14
+ def __init__(self, globals_dict=None):
15
+ self.retry = False
16
+ if globals_dict is None:
17
+ globals_dict = globals()
18
+ self.globals_dict = globals_dict
19
+ self.lock = global_config.runner_lock
20
+
21
+
22
+ def run_single_step(self, component, method, kwargs, wait=True, current_app=None):
23
+ global deck
24
+ if deck is None:
25
+ deck = global_config.deck
26
+
27
+ # Try to acquire lock without blocking
28
+ if not self.lock.acquire(blocking=False):
29
+ current_status = global_config.runner_status
30
+ current_status["status"] = "busy"
31
+ return current_status
32
+
33
+
34
+ if wait:
35
+ output = self._run_single_step(component, method, kwargs, current_app)
36
+ else:
37
+ print("running with thread")
38
+ thread = threading.Thread(
39
+ target=self._run_single_step, args=(component, method, kwargs, current_app)
40
+ )
41
+ thread.start()
42
+ time.sleep(0.1)
43
+ output = {"status": "task started", "task_id": global_config.runner_status.get("id")}
44
+
45
+ return output
46
+
47
+ def _get_executable(self, component, deck, method):
48
+ if component.startswith("deck."):
49
+ component = component.split(".")[1]
50
+ instrument = getattr(deck, component)
51
+ else:
52
+ temp_connections = global_config.defined_variables
53
+ instrument = temp_connections.get(component)
54
+ function_executable = getattr(instrument, method)
55
+ return function_executable
56
+
57
+ def _run_single_step(self, component, method, kwargs, current_app=None):
58
+ try:
59
+ function_executable = self._get_executable(component, deck, method)
60
+ method_name = f"{function_executable.__self__.__class__.__name__}.{function_executable.__name__}"
61
+ except Exception as e:
62
+ self.lock.release()
63
+ return {"status": "error", "msg": e.__str__()}
64
+
65
+ # with self.lock:
66
+ with current_app.app_context():
67
+ step = SingleStep(method_name=method_name, kwargs=kwargs, run_error=False, start_time=datetime.now())
68
+ db.session.add(step)
69
+ db.session.commit()
70
+ global_config.runner_status = {"id":step.id, "type": "task"}
71
+ try:
72
+ output = function_executable(**kwargs)
73
+ step.output = output
74
+ step.end_time = datetime.now()
75
+ except Exception as e:
76
+ step.run_error = e.__str__()
77
+ step.end_time = datetime.now()
78
+ finally:
79
+ db.session.commit()
80
+ self.lock.release()
81
+ return output
ivoryos/utils/utils.py CHANGED
@@ -240,74 +240,6 @@ def start_logger(socketio: SocketIO, logger_name: str, log_filename: str = None)
240
240
  return logger
241
241
 
242
242
 
243
- def ax_wrapper(data: dict, arg_types: list):
244
- """
245
- Ax platform wrapper function for creating optimization campaign parameters and objective from the web form input
246
- :param data: e.g.,
247
- {
248
- "param_1_type": "range", "param_1_value": [1,2],
249
- "param_2_type": "range", "param_2_value": [1,2],
250
- "obj_1_min": True,
251
- "obj_2_min": True
252
- }
253
- :return: the optimization campaign parameters
254
- parameter=[
255
- {"name": "param_1", "type": "range", "bounds": [1,2]},
256
- {"name": "param_1", "type": "range", "bounds": [1,2]}
257
- ]
258
- objectives=[
259
- {"name": "obj_1", "min": True, "threshold": None},
260
- {"name": "obj_2", "min": True, "threshold": None},
261
- ]
262
- """
263
- from ax.service.utils.instantiation import ObjectiveProperties
264
- parameter = []
265
- objectives = {}
266
- # Iterate through the webui_data dictionary
267
- for key, value in data.items():
268
- # Check if the key corresponds to a parameter type
269
- if "_type" in key:
270
- param_name = key.split("_type")[0]
271
- param_type = value
272
- param_value = data[f"{param_name}_value"].split(",")
273
- try:
274
- values = [float(v) for v in param_value]
275
- except Exception:
276
- values = param_value
277
- if param_type == "range":
278
- param = {"name": param_name, "type": param_type, "bounds": values}
279
- if param_type == "choice":
280
- param = {"name": param_name, "type": param_type, "values": values}
281
- if param_type == "fixed":
282
- param = {"name": param_name, "type": param_type, "value": values[0]}
283
- _type = arg_types[param_name] if arg_types[param_name] in ["str", "bool", "int"] else "float"
284
- param.update({"value_type": _type})
285
- parameter.append(param)
286
- elif key.endswith("_min"):
287
- if not value == 'none':
288
- obj_name = key.split("_min")[0]
289
- is_min = True if value == "minimize" else False
290
-
291
- threshold = None if f"{obj_name}_threshold" not in data else data[f"{obj_name}_threshold"]
292
- properties = ObjectiveProperties(minimize=is_min)
293
- objectives[obj_name] = properties
294
-
295
- return parameter, objectives
296
-
297
-
298
- def ax_initiation(data, arg_types):
299
- """
300
- create Ax campaign from the web form input
301
- :param data:
302
- """
303
- install_and_import("ax", "ax-platform")
304
- parameter, objectives = ax_wrapper(data, arg_types)
305
- from ax.service.ax_client import AxClient
306
- ax_client = AxClient()
307
- ax_client.create_experiment(parameter, objectives=objectives)
308
- return ax_client
309
-
310
-
311
243
  def get_arg_type(args, parameters):
312
244
  """get argument type from signature"""
313
245
  arg_types = {}
ivoryos/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.25"
1
+ __version__ = "1.0.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ivoryos
3
- Version: 0.1.25
3
+ Version: 1.0.1
4
4
  Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
5
  Home-page: https://gitlab.com/heingroup/ivoryos
6
6
  Author: Ivory Zhang
@@ -22,8 +22,8 @@ Requires-Dist: python-dotenv
22
22
  [![PyPI version](https://img.shields.io/pypi/v/ivoryos)](https://pypi.org/project/ivoryos/)
23
23
  ![License](https://img.shields.io/pypi/l/ivoryos)
24
24
  [![YouTube](https://img.shields.io/badge/YouTube-video-red?logo=youtube)](https://youtu.be/dFfJv9I2-1g)
25
- [![Published](https://img.shields.io/badge/Nature_Communications-paper-blue)](https://www.nature.com/articles/s41467-025-60514-w)
26
-
25
+ [![Published](https://img.shields.io/badge/Nature_Comm.-paper-blue)](https://www.nature.com/articles/s41467-025-60514-w)
26
+ [![Discord](https://img.shields.io/discord/1313641159356059770?label=Discord&logo=discord&color=5865F2)](https://discord.gg/AX5P9EdGVX)
27
27
 
28
28
  ![](https://gitlab.com/heingroup/ivoryos/raw/main/docs/source/_static/ivoryos.png)
29
29
  # ivoryOS: interoperable Web UI for self-driving laboratories (SDLs)
@@ -1,26 +1,26 @@
1
1
  ivoryos/__init__.py,sha256=1v6LwuIao8WbmSskifzdfde7E_gH3PWCYaNpLaRtYZk,7341
2
2
  ivoryos/config.py,sha256=3FPBYTIBhQTKDvsEoR8ZeTmg65D-CSFEdGmOuIL4pSI,1311
3
- ivoryos/version.py,sha256=Ej7LsXg-6CASlaEHsZkUoLDpYEfHeFKdIeXMIM0esgA,23
3
+ ivoryos/version.py,sha256=d4QHYmS_30j0hPN8NmNPnQ_Z0TphDRbu4MtQj9cT9e8,22
4
4
  ivoryos/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  ivoryos/routes/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  ivoryos/routes/auth/auth.py,sha256=7CdXjGAr1B_xsmwweakTWOoROgsOJf0MNTzlMP_5Nus,3240
7
7
  ivoryos/routes/auth/templates/auth/login.html,sha256=WSRrKbdM_oobqSXFRTo-j9UlOgp6sYzS9tm7TqqPULI,1207
8
8
  ivoryos/routes/auth/templates/auth/signup.html,sha256=b5LTXtpfTSkSS7X8u1ldwQbbgEFTk6UNMAediA5BwBY,1465
9
9
  ivoryos/routes/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- ivoryos/routes/control/control.py,sha256=bxNYR-VAFA7tSswDt5k130AnIx1f64_R9N2B9kMridI,14160
10
+ ivoryos/routes/control/control.py,sha256=AbJA1dwWAWZi3eGnsbNObPASVsN7pZbL6la25xB52hI,14209
11
11
  ivoryos/routes/control/templates/control/controllers.html,sha256=iIp0h6WA68gQj9OsoiB7dU1BqH8CGomTueR73F4C8eY,4274
12
12
  ivoryos/routes/control/templates/control/controllers_home.html,sha256=VQ77HRvBlyBrQ3al5fcKF5Y6_vKtU8WeAhilqQQltAo,2997
13
13
  ivoryos/routes/control/templates/control/controllers_new.html,sha256=uOQo9kYmwX2jk3KZDkMUF_ylfNUIs_oIWb_kk_MMVDM,4921
14
14
  ivoryos/routes/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- ivoryos/routes/database/database.py,sha256=uchoLXRx8ShJZIu20NZya6FJ4LMg2b9yvn6hjEq3dlU,7687
15
+ ivoryos/routes/database/database.py,sha256=97387fF6tQIfb-BF6krILRP4a4_8jOJmGcE7BoPgjmk,9127
16
16
  ivoryos/routes/database/templates/database/experiment_database.html,sha256=edlCcKfrS91gGG1dPFQjC9xD7F7nWNNqS3S6Oa7apzs,3460
17
17
  ivoryos/routes/database/templates/database/experiment_step_view.html,sha256=u8_XYhiZ98PzglMzFEkuM1Tk9hVWf79xXIrpHVDxKa0,3618
18
18
  ivoryos/routes/database/templates/database/step_card.html,sha256=F4JRfacrEQfk2rrEbcI_i7G84nzKKDmCrMSmStLb4W4,290
19
19
  ivoryos/routes/database/templates/database/workflow_run_database.html,sha256=MczK9my9u0SyQsMFLbc6CXeZqKaBo5vk1SpwjkcZdqk,3571
20
20
  ivoryos/routes/design/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- ivoryos/routes/design/design.py,sha256=ViVCys-yPv_W4B2JrLT9A4jf8_2N_QDHgbEVUwGFre4,25334
21
+ ivoryos/routes/design/design.py,sha256=ameAcgTLlrfOENUa48xxp64psuh2aZ0JuVfIj7zj-LM,28968
22
22
  ivoryos/routes/design/templates/design/experiment_builder.html,sha256=rEdcHj5onJG_4MejdFBPnJVzsvCMp1KDteqNkpx24kQ,29430
23
- ivoryos/routes/design/templates/design/experiment_run.html,sha256=Q7cYYTgvZ8SBzqkDEhAwR634-LLcYq4Gof4bpH_adt0,30397
23
+ ivoryos/routes/design/templates/design/experiment_run.html,sha256=7VP0Vo98phcYnFennd5vqaMK1M1QBwDmM-b9aZb8jOw,26282
24
24
  ivoryos/routes/main/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  ivoryos/routes/main/main.py,sha256=yuVJzXAob1kc1dfflkTBIZQ0tdf6kChfuq-uQlN1e9Q,957
26
26
  ivoryos/routes/main/templates/main/help.html,sha256=IOktMEsOPk0SCiMBXZ4mpffClERAyX8W82fel71M3M0,9370
@@ -31,20 +31,22 @@ ivoryos/static/style.css,sha256=zQVx35A5g6JMJ-K84-6fSKtzXGjp_p5ZVG6KLHPM2IE,4021
31
31
  ivoryos/static/gui_annotation/Slide1.png,sha256=Lm4gdOkUF5HIUFaB94tl6koQVkzpitKj43GXV_XYMMc,121727
32
32
  ivoryos/static/gui_annotation/Slide2.PNG,sha256=z3wQ9oVgg4JTWVLQGKK_KhtepRHUYP1e05XUWGT2A0I,118761
33
33
  ivoryos/static/js/overlay.js,sha256=dPxop19es0E0ZUSY3d_4exIk7CJuQEnlW5uTt5fZfzI,483
34
- ivoryos/static/js/socket_handler.js,sha256=YGwWlT8TqNBvvIzs2G9g1g7nM2-vUPZjCwmQt4Yv0Uw,5078
34
+ ivoryos/static/js/socket_handler.js,sha256=2Iyv_3METjhSlSavs_L9FE3PKY4xDEpfzJpd2FywY9o,5300
35
35
  ivoryos/static/js/sortable_card.js,sha256=ifmlGe3yy0U_KzMphV4ClRhK2DLOvkELYMlq1vECuac,807
36
36
  ivoryos/static/js/sortable_design.js,sha256=wwpKfIzZGDxfX3moNz0cvPvm9YyHmopZK3wmkUdnBiw,4333
37
37
  ivoryos/templates/base.html,sha256=sDdwqOIUP2Get-py4E59PkieoGWLFpX6wAJe93s4aRo,8518
38
38
  ivoryos/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
+ ivoryos/utils/bo_campaign.py,sha256=CVs7q15Pm2SRuJNaCvZKIxOFuv1xibM2yymtpAMAWOk,3285
39
40
  ivoryos/utils/client_proxy.py,sha256=AzcSQGMqeCqVULP1a7vEKNe135NZYryVX63ke0wgK04,2099
40
- ivoryos/utils/db_models.py,sha256=1G2De1nLRCGwuY4zqgMeIQ-p1XJ_PkBxH1cd0fJ9YgY,26740
41
+ ivoryos/utils/db_models.py,sha256=zlmmD2600CYyn79gQq8k0Vra7BDBKJBAyNLYclIWdvs,27382
41
42
  ivoryos/utils/form.py,sha256=b3JKxRc1jN45-bXyfzSJT1lcssUuxT86FhRmNUDv5-U,20973
42
- ivoryos/utils/global_config.py,sha256=P0xs_33bZfNQ-D71lCkq7HJyT4ngQWPqUKnkoMrmM8c,1908
43
+ ivoryos/utils/global_config.py,sha256=OqfDrPgOzRdIUMD4V3pA9t6b-BATMjGZl8Jn7nkI56k,2138
43
44
  ivoryos/utils/llm_agent.py,sha256=-lVCkjPlpLues9sNTmaT7bT4sdhWvV2DiojNwzB2Lcw,6422
44
- ivoryos/utils/script_runner.py,sha256=WkLGO0tI6Bh1iJatJgYt00LcjoOYdAPCjDfYbyvqRN8,13649
45
- ivoryos/utils/utils.py,sha256=pVdhe3RksaxwRLEaKq-Q1hl7oMi9f0K1LpPusevak2s,16234
46
- ivoryos-0.1.25.dist-info/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
47
- ivoryos-0.1.25.dist-info/METADATA,sha256=76PNWOyr0PViE935EeFrmjY8JZ749sxgEri3pWcDokE,6867
48
- ivoryos-0.1.25.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
49
- ivoryos-0.1.25.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
50
- ivoryos-0.1.25.dist-info/RECORD,,
45
+ ivoryos/utils/script_runner.py,sha256=k7bH9AeFGhx2tns-81q0JZFLCM9cvXCpZF7bH80RVzo,14504
46
+ ivoryos/utils/task_runner.py,sha256=u4nF0wOADu_HVlGYVTOXnUm1woWGgYAccr-ZCzgtb6Q,2899
47
+ ivoryos/utils/utils.py,sha256=OBwrRu02yh7pqG_lyl10zWr_RYes3xhMporxIz8lGYI,13579
48
+ ivoryos-1.0.1.dist-info/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
49
+ ivoryos-1.0.1.dist-info/METADATA,sha256=KEh9s448whM6VP-Knc5UUjl7DpoME989pKBfwLxFcHM,6992
50
+ ivoryos-1.0.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
51
+ ivoryos-1.0.1.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
52
+ ivoryos-1.0.1.dist-info/RECORD,,