ivoryos 1.2.5__py3-none-any.whl → 1.4.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. docs/source/conf.py +84 -0
  2. ivoryos/__init__.py +16 -246
  3. ivoryos/app.py +154 -0
  4. ivoryos/optimizer/ax_optimizer.py +55 -28
  5. ivoryos/optimizer/base_optimizer.py +20 -1
  6. ivoryos/optimizer/baybe_optimizer.py +27 -17
  7. ivoryos/optimizer/nimo_optimizer.py +173 -0
  8. ivoryos/optimizer/registry.py +3 -1
  9. ivoryos/routes/auth/auth.py +35 -8
  10. ivoryos/routes/auth/templates/change_password.html +32 -0
  11. ivoryos/routes/control/control.py +58 -28
  12. ivoryos/routes/control/control_file.py +12 -15
  13. ivoryos/routes/control/control_new_device.py +21 -11
  14. ivoryos/routes/control/templates/controllers.html +27 -0
  15. ivoryos/routes/control/utils.py +2 -0
  16. ivoryos/routes/data/data.py +110 -44
  17. ivoryos/routes/data/templates/components/step_card.html +78 -13
  18. ivoryos/routes/data/templates/workflow_view.html +343 -113
  19. ivoryos/routes/design/design.py +59 -10
  20. ivoryos/routes/design/design_file.py +3 -3
  21. ivoryos/routes/design/design_step.py +43 -17
  22. ivoryos/routes/design/templates/components/action_form.html +2 -2
  23. ivoryos/routes/design/templates/components/canvas_main.html +6 -1
  24. ivoryos/routes/design/templates/components/edit_action_form.html +18 -3
  25. ivoryos/routes/design/templates/components/info_modal.html +318 -0
  26. ivoryos/routes/design/templates/components/instruments_panel.html +23 -1
  27. ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
  28. ivoryos/routes/design/templates/experiment_builder.html +3 -0
  29. ivoryos/routes/execute/execute.py +82 -22
  30. ivoryos/routes/execute/templates/components/logging_panel.html +50 -25
  31. ivoryos/routes/execute/templates/components/run_tabs.html +45 -2
  32. ivoryos/routes/execute/templates/components/tab_bayesian.html +447 -325
  33. ivoryos/routes/execute/templates/components/tab_configuration.html +303 -18
  34. ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
  35. ivoryos/routes/execute/templates/experiment_run.html +0 -264
  36. ivoryos/routes/library/library.py +9 -11
  37. ivoryos/routes/main/main.py +30 -2
  38. ivoryos/server.py +180 -0
  39. ivoryos/socket_handlers.py +1 -1
  40. ivoryos/static/ivoryos_logo.png +0 -0
  41. ivoryos/static/js/action_handlers.js +259 -88
  42. ivoryos/static/js/socket_handler.js +40 -5
  43. ivoryos/static/js/sortable_design.js +29 -11
  44. ivoryos/templates/base.html +61 -2
  45. ivoryos/utils/bo_campaign.py +18 -17
  46. ivoryos/utils/client_proxy.py +267 -36
  47. ivoryos/utils/db_models.py +286 -60
  48. ivoryos/utils/decorators.py +34 -0
  49. ivoryos/utils/form.py +52 -19
  50. ivoryos/utils/global_config.py +21 -0
  51. ivoryos/utils/nest_script.py +314 -0
  52. ivoryos/utils/py_to_json.py +80 -10
  53. ivoryos/utils/script_runner.py +573 -189
  54. ivoryos/utils/task_runner.py +69 -22
  55. ivoryos/utils/utils.py +48 -5
  56. ivoryos/version.py +1 -1
  57. {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/METADATA +109 -47
  58. ivoryos-1.4.4.dist-info/RECORD +119 -0
  59. ivoryos-1.4.4.dist-info/top_level.txt +3 -0
  60. tests/__init__.py +0 -0
  61. tests/conftest.py +133 -0
  62. tests/integration/__init__.py +0 -0
  63. tests/integration/test_route_auth.py +80 -0
  64. tests/integration/test_route_control.py +94 -0
  65. tests/integration/test_route_database.py +61 -0
  66. tests/integration/test_route_design.py +36 -0
  67. tests/integration/test_route_main.py +35 -0
  68. tests/integration/test_sockets.py +26 -0
  69. tests/unit/test_type_conversion.py +42 -0
  70. tests/unit/test_util.py +3 -0
  71. ivoryos/routes/api/api.py +0 -56
  72. ivoryos-1.2.5.dist-info/RECORD +0 -100
  73. ivoryos-1.2.5.dist-info/top_level.txt +0 -1
  74. {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +0 -0
  75. {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/licenses/LICENSE +0 -0
@@ -3,13 +3,13 @@ import os
3
3
  import time
4
4
 
5
5
  from flask import Blueprint, redirect, url_for, flash, jsonify, request, render_template, session, \
6
- current_app, g
6
+ current_app, g, send_file
7
7
  from flask_login import login_required
8
8
 
9
9
  from ivoryos.routes.execute.execute_file import files
10
10
  from ivoryos.utils import utils
11
11
  from ivoryos.utils.bo_campaign import parse_optimization_form
12
- from ivoryos.utils.db_models import SingleStep, WorkflowRun, WorkflowStep
12
+ from ivoryos.utils.db_models import SingleStep, WorkflowRun, WorkflowStep, WorkflowPhase
13
13
  from ivoryos.utils.global_config import GlobalConfig
14
14
  from ivoryos.utils.form import create_action_button
15
15
 
@@ -76,14 +76,17 @@ def experiment_run():
76
76
  if isinstance(exec_string, dict):
77
77
  for key, func_str in exec_string.items():
78
78
  exec(func_str)
79
- line_collection = script.convert_to_lines(exec_string)
79
+
80
80
  else:
81
81
  # Handle string case - you might need to adjust this based on your needs
82
- line_collection = []
82
+ line_collection = {}
83
83
  except Exception:
84
84
  flash(f"Please check syntax!!")
85
85
  return redirect(url_for("design.experiment_builder"))
86
86
 
87
+
88
+ line_collection = script.render_script_lines(script.script_dict)
89
+
87
90
  run_name = script.name if script.name else "untitled"
88
91
 
89
92
  dismiss = session.get("dismiss", None)
@@ -92,8 +95,13 @@ def experiment_run():
92
95
 
93
96
  _, return_list = script.config_return()
94
97
  config_list, config_type_list = script.config("script")
95
- data_list = os.listdir(current_app.config['DATA_FOLDER'])
96
- data_list.remove(".gitkeep") if ".gitkeep" in data_list else data_list
98
+ data_list = [f for f in os.listdir(current_app.config['DATA_FOLDER']) if f.endswith('.csv')]
99
+ # Remove .gitkeep if present
100
+ if ".gitkeep" in data_list:
101
+ data_list.remove(".gitkeep")
102
+
103
+ # Sort by creation time, newest first
104
+ data_list.sort(key=lambda f: os.path.getctime(os.path.join(current_app.config['DATA_FOLDER'], f)), reverse=True)
97
105
 
98
106
  if deck is None:
99
107
  no_deck_warning = True
@@ -105,31 +113,36 @@ def experiment_run():
105
113
  flash(f"This script is not compatible with current deck, import {script.deck}")
106
114
 
107
115
  if request.method == "POST":
108
- bo_args = None
116
+ # bo_args = None
109
117
  compiled = False
110
118
  if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
111
119
  payload_json = request.get_json()
112
120
  compiled = True
113
121
  if "kwargs" in payload_json:
114
122
  config = payload_json["kwargs"]
115
- elif "parameters" in payload_json:
116
- bo_args = payload_json
123
+ # elif "parameters" in payload_json:
124
+ # bo_args = payload_json
117
125
  repeat = payload_json.pop("repeat", None)
126
+ batch_size = payload_json.pop('batch_size', 1)
118
127
  else:
119
128
  if "bo" in request.form:
120
129
  bo_args = request.form.to_dict()
121
130
  existing_data = bo_args.pop("existing_data")
122
131
  if "online-config" in request.form:
123
- config = utils.web_config_entry_wrapper(request.form.to_dict(), config_list)
132
+ config_args = request.form.to_dict()
133
+ config_args.pop("batch_size", None)
134
+ config = utils.web_config_entry_wrapper(config_args, config_list)
135
+ batch_size = int(request.form.get('batch_size', 1))
124
136
  repeat = request.form.get('repeat', None)
125
137
 
126
138
  try:
139
+ # if True:
127
140
  datapath = current_app.config["DATA_FOLDER"]
128
141
  run_name = script.validate_function_name(run_name)
129
- runner.run_script(script=script, run_name=run_name, config=config, bo_args=bo_args,
142
+ runner.run_script(script=script, run_name=run_name, config=config,
130
143
  logger=g.logger, socketio=g.socketio, repeat_count=repeat,
131
144
  output_path=datapath, compiled=compiled, history=existing_data,
132
- current_app=current_app._get_current_object()
145
+ current_app=current_app._get_current_object(), batch_size=batch_size
133
146
  )
134
147
  if utils.check_config_duplicate(config):
135
148
  flash(f"WARNING: Duplicate in config entries.")
@@ -157,6 +170,7 @@ def experiment_run():
157
170
  def run_bo():
158
171
  """
159
172
  .. :quickref: Workflow Execution; run Bayesian Optimization
173
+
160
174
  Run Bayesian Optimization with the given parameters and objectives.
161
175
 
162
176
  .. http:post:: /executions/campaign
@@ -166,27 +180,56 @@ def run_bo():
166
180
  :form existing_data: existing data to use for optimization
167
181
  :form parameters: parameters for optimization
168
182
  :form objectives: objectives for optimization
183
+
169
184
  TODO: merge to experiment_run or not, add more details about the form fields and their expected values.
170
185
  """
171
186
  script = utils.get_script_file()
172
187
  run_name = script.name if script.name else "untitled"
173
- payload = request.form.to_dict()
174
- repeat = payload.pop("repeat", None)
175
- optimizer_type = payload.pop("optimizer_type", None)
176
- existing_data = payload.pop("existing_data", None)
177
- parameters, objectives, steps = parse_optimization_form(payload)
188
+
189
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
190
+ payload_json = request.get_json()
191
+ objectives = payload_json.pop("objectives", None)
192
+ parameters = payload_json.pop("parameters", None)
193
+ steps = payload_json.pop("steps", None)
194
+ constraints = payload_json.pop("parameter_constraints", None)
195
+ repeat = payload_json.pop("repeat", None)
196
+ batch_size = payload_json.pop("batch_size", None)
197
+ optimizer_type = payload_json.pop("optimizer_type", None)
198
+ existing_data = payload_json.pop("existing_data", None)
199
+
200
+ else:
201
+ payload = request.form.to_dict()
202
+ repeat = payload.pop("repeat", None)
203
+ optimizer_type = payload.pop("optimizer_type", None)
204
+ existing_data = payload.pop("existing_data", None)
205
+ batch_mode = payload.pop("batch_mode", None)
206
+ batch_size = payload.pop("batch_size", 1)
207
+
208
+ # Get constraint expressions (new single-line input)
209
+ constraint_exprs = request.form.getlist("constraint_expr")
210
+ constraints = [expr.strip() for expr in constraint_exprs if expr.strip()]
211
+
212
+ # Remove constraint_expr entries from payload before parsing parameters
213
+ for key in list(payload.keys()):
214
+ if key.startswith("constraint_expr"):
215
+ payload.pop(key, None)
216
+
217
+ parameters, objectives, steps = parse_optimization_form(payload)
218
+
219
+ # if True:
178
220
  try:
179
221
  datapath = current_app.config["DATA_FOLDER"]
180
222
  run_name = script.validate_function_name(run_name)
181
223
  Optimizer = global_config.optimizers.get(optimizer_type, None)
182
224
  if not Optimizer:
183
225
  raise ValueError(f"Optimizer {optimizer_type} is not supported or not found.")
184
- optimizer = Optimizer(experiment_name=run_name, parameter_space=parameters, objective_config=objectives,
185
- optimizer_config=steps)
186
- runner.run_script(script=script, run_name=run_name, optimizer=optimizer,
226
+
227
+ runner.run_script(script=script, run_name=run_name, optimizer=None,
187
228
  logger=g.logger, socketio=g.socketio, repeat_count=repeat,
188
229
  output_path=datapath, compiled=False, history=existing_data,
189
- current_app=current_app._get_current_object()
230
+ current_app=current_app._get_current_object(), batch_size=int(batch_size),
231
+ objectives=objectives, parameters=parameters, constraints=constraints, steps=steps,
232
+ optimizer_cls=Optimizer
190
233
  )
191
234
 
192
235
  except Exception as e:
@@ -196,6 +239,21 @@ def run_bo():
196
239
  flash(e.__str__())
197
240
  return redirect(url_for("execute.experiment_run"))
198
241
 
242
+ @execute.route("/executions/latest_plot")
243
+ @login_required
244
+ def get_optimizer_plot():
245
+
246
+ optimizer = current_app.config.get("LAST_OPTIMIZER")
247
+ if optimizer is not None:
248
+ # the placeholder is for showing different plots
249
+ latest_file = optimizer.get_plots('placeholder')
250
+ # print(latest_file)
251
+ if files:
252
+ return send_file(latest_file, mimetype="image/png")
253
+ # print("No plots found")
254
+ return jsonify({"error": "No plots found"}), 404
255
+
256
+
199
257
 
200
258
 
201
259
  @execute.route("/executions/status", methods=["GET"])
@@ -225,7 +283,9 @@ def runner_status():
225
283
  if task_type == "workflow":
226
284
  workflow = WorkflowRun.query.get(task_id)
227
285
  if workflow is not None:
228
- latest_step = WorkflowStep.query.filter_by(workflow_id=workflow.id).order_by(
286
+ phases = WorkflowPhase.query.filter_by(run_id=workflow.id).order_by(WorkflowPhase.start_time).all()
287
+ current_phase = phases[-1]
288
+ latest_step = WorkflowStep.query.filter_by(phase_id=current_phase.id).order_by(
229
289
  WorkflowStep.start_time.desc()).first()
230
290
  if latest_step is not None:
231
291
  current_step = latest_step.as_dict()
@@ -1,31 +1,56 @@
1
1
  {# Logging panel component for experiment run #}
2
2
  <div class="col-lg-6 col-sm-12 logging-panel">
3
- <p>
4
- <div class="p d-flex justify-content-between align-items-center">
5
- <h5>Progress:</h5>
6
- <div class="d-flex gap-2 ms-auto">
7
- <button id="pause-resume" class="btn btn-info text-white" data-bs-toggle="tooltip" title="Pause execution">
8
- {% if pause_status %}
9
- <i class="bi bi-play-circle"></i>
10
- {% else %}
11
- <i class="bi bi-pause-circle"></i>
12
- {% endif %}
13
- </button>
14
- <button id="abort-current" class="btn btn-danger text-white" data-bs-toggle="tooltip" title="Stop execution after current step">
15
- <i class="bi bi-stop-circle"></i>
16
- </button>
17
- <button id="abort-pending" class="btn btn-warning text-white" data-bs-toggle="tooltip" title="Stop execution after current iteration">
18
- <i class="bi bi-hourglass-split"></i>
19
- </button>
20
- </div>
3
+ <div class="d-flex justify-content-between align-items-center">
4
+ <h5>Progress:</h5>
5
+ <div class="d-flex gap-2 ms-auto">
6
+ <button id="pause-resume" class="btn btn-info text-white">
7
+ {% if pause_status %}
8
+ <i class="bi bi-play-circle"></i>
9
+ {% else %}
10
+ <i class="bi bi-pause-circle"></i>
11
+ {% endif %}
12
+ </button>
13
+ <button id="abort-current" class="btn btn-danger text-white">
14
+ <i class="bi bi-stop-circle"></i>
15
+ </button>
16
+ <button id="abort-pending" class="btn btn-warning text-white">
17
+ <i class="bi bi-hourglass-split"></i>
18
+ </button>
21
19
  </div>
22
- <div class="text-muted mt-2">
23
- <small><strong>Note:</strong> The current step cannot be paused or stopped until it completes. </small>
20
+ </div>
21
+ <small class="text-muted mt-2">
22
+ <strong>Note:</strong> The current step cannot be paused or stopped until it completes.
23
+ </small>
24
+ <div class="progress my-3">
25
+ <div id="progress-bar-inner" class="progress-bar progress-bar-striped progress-bar-animated"></div>
26
+ </div>
27
+ <!-- Tabs -->
28
+ <ul class="nav nav-tabs" id="logPlotTabs" role="tablist">
29
+ <li class="nav-item" role="presentation">
30
+ <button class="nav-link active" id="log-tab" data-bs-toggle="tab" data-bs-target="#log" type="button" role="tab">Log</button>
31
+ </li>
32
+ <li class="nav-item" role="presentation">
33
+ <button class="nav-link" id="plot-tab" data-bs-toggle="tab" data-bs-target="#plot" type="button" role="tab">Optimizer Plot</button>
34
+ </li>
35
+ </ul>
36
+ <div class="tab-content mt-3" id="logPlotTabsContent">
37
+ <div class="tab-pane fade show active" id="log" role="tabpanel">
38
+ <div id="logging-panel" class="border p-2 bg-light" style="max-height: 400px; overflow-y: auto;"></div>
39
+ </div>
40
+ <div class="tab-pane fade" id="plot" role="tabpanel">
41
+ <button class="btn btn-success mb-2" onclick="showPlot()">Refresh Plot</button>
42
+ <small class="text-muted d-block mt-1" id="plot-info">This function is only available for NIMO optimizers.</small>
43
+ <br>
44
+ <img id="optimizerPlot" src="" class="img-fluid rounded shadow-sm d-block mx-auto" style="max-width:100%; display:none;">
24
45
  </div>
25
46
 
26
- <div class="progress" role="progressbar" aria-label="Animated striped example" aria-valuenow="10" aria-valuemin="0" aria-valuemax="100">
27
- <div id="progress-bar-inner" class="progress-bar progress-bar-striped progress-bar-animated"></div>
28
47
  </div>
29
- <p><h5>Log:</h5></p>
30
- <div id="logging-panel"></div>
31
- </div>
48
+
49
+ </div>
50
+ <script>
51
+ function showPlot() {
52
+ const img = document.getElementById('optimizerPlot');
53
+ img.src = "{{ url_for('execute.get_optimizer_plot') }}";
54
+ img.style.display = 'block';
55
+ }
56
+ </script>
@@ -1,4 +1,4 @@
1
- {# Run tabs component for experiment run #}
1
+ <!-- Main template structure -->
2
2
  <ul class="nav nav-tabs" id="myTabs" role="tablist">
3
3
  <li class="nav-item" role="presentation">
4
4
  <a class="nav-link {{ 'disabled' if config_list else '' }} {{ 'active' if not config_list else '' }}" id="tab1-tab" data-bs-toggle="tab" href="#tab1" role="tab" aria-controls="tab1" aria-selected="false">Repeat</a>
@@ -14,4 +14,47 @@
14
14
  {% include 'components/tab_repeat.html' %}
15
15
  {% include 'components/tab_configuration.html' %}
16
16
  {% include 'components/tab_bayesian.html' %}
17
- </div>
17
+ </div>
18
+
19
+ <!-- ============================================ -->
20
+ <!-- SWITCH TO THE LAST ACTIVE TAB ON PAGE LOAD -->
21
+ <!-- ============================================ -->
22
+ <script>
23
+ (function() {
24
+ 'use strict';
25
+
26
+ // Store active tab when changed
27
+ document.addEventListener('shown.bs.tab', function(e) {
28
+ const tabId = e.target.id.replace('-tab', '');
29
+ if (tabId === 'tab2' || tabId === 'tab3') {
30
+ localStorage.setItem('ivoryosLastTab', tabId);
31
+ console.log('Saved last active tab:', tabId);
32
+ }
33
+
34
+ // Optional: run specific load logic per tab
35
+ if (tabId === 'tab2' && typeof window.loadConfigData === 'function') {
36
+ window.loadConfigData();
37
+ } else if (tabId === 'tab3' && typeof window.saveBO === 'function') {
38
+ window.saveBO();
39
+ }
40
+ });
41
+
42
+ // Restore tab from last session
43
+ document.addEventListener('DOMContentLoaded', function() {
44
+ const lastTab = localStorage.getItem('ivoryosLastTab');
45
+ const tabElement = lastTab ? document.getElementById(lastTab + '-tab') : null;
46
+
47
+ if (tabElement && !tabElement.classList.contains('disabled')) {
48
+ new bootstrap.Tab(tabElement).show();
49
+ console.log('Restored last active tab:', lastTab);
50
+ } else {
51
+ // Default to first non-disabled tab
52
+ const firstTab = document.querySelector('#myTabs .nav-link:not(.disabled)');
53
+ if (firstTab) {
54
+ new bootstrap.Tab(firstTab).show();
55
+ console.log('Defaulted to first available tab.');
56
+ }
57
+ }
58
+ });
59
+ })();
60
+ </script>