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.
- docs/source/conf.py +84 -0
- ivoryos/__init__.py +16 -246
- ivoryos/app.py +154 -0
- ivoryos/optimizer/ax_optimizer.py +55 -28
- ivoryos/optimizer/base_optimizer.py +20 -1
- ivoryos/optimizer/baybe_optimizer.py +27 -17
- ivoryos/optimizer/nimo_optimizer.py +173 -0
- ivoryos/optimizer/registry.py +3 -1
- ivoryos/routes/auth/auth.py +35 -8
- ivoryos/routes/auth/templates/change_password.html +32 -0
- ivoryos/routes/control/control.py +58 -28
- ivoryos/routes/control/control_file.py +12 -15
- ivoryos/routes/control/control_new_device.py +21 -11
- ivoryos/routes/control/templates/controllers.html +27 -0
- ivoryos/routes/control/utils.py +2 -0
- ivoryos/routes/data/data.py +110 -44
- ivoryos/routes/data/templates/components/step_card.html +78 -13
- ivoryos/routes/data/templates/workflow_view.html +343 -113
- ivoryos/routes/design/design.py +59 -10
- ivoryos/routes/design/design_file.py +3 -3
- ivoryos/routes/design/design_step.py +43 -17
- ivoryos/routes/design/templates/components/action_form.html +2 -2
- ivoryos/routes/design/templates/components/canvas_main.html +6 -1
- ivoryos/routes/design/templates/components/edit_action_form.html +18 -3
- ivoryos/routes/design/templates/components/info_modal.html +318 -0
- ivoryos/routes/design/templates/components/instruments_panel.html +23 -1
- ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
- ivoryos/routes/design/templates/experiment_builder.html +3 -0
- ivoryos/routes/execute/execute.py +82 -22
- ivoryos/routes/execute/templates/components/logging_panel.html +50 -25
- ivoryos/routes/execute/templates/components/run_tabs.html +45 -2
- ivoryos/routes/execute/templates/components/tab_bayesian.html +447 -325
- ivoryos/routes/execute/templates/components/tab_configuration.html +303 -18
- ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
- ivoryos/routes/execute/templates/experiment_run.html +0 -264
- ivoryos/routes/library/library.py +9 -11
- ivoryos/routes/main/main.py +30 -2
- ivoryos/server.py +180 -0
- ivoryos/socket_handlers.py +1 -1
- ivoryos/static/ivoryos_logo.png +0 -0
- ivoryos/static/js/action_handlers.js +259 -88
- ivoryos/static/js/socket_handler.js +40 -5
- ivoryos/static/js/sortable_design.js +29 -11
- ivoryos/templates/base.html +61 -2
- ivoryos/utils/bo_campaign.py +18 -17
- ivoryos/utils/client_proxy.py +267 -36
- ivoryos/utils/db_models.py +286 -60
- ivoryos/utils/decorators.py +34 -0
- ivoryos/utils/form.py +52 -19
- ivoryos/utils/global_config.py +21 -0
- ivoryos/utils/nest_script.py +314 -0
- ivoryos/utils/py_to_json.py +80 -10
- ivoryos/utils/script_runner.py +573 -189
- ivoryos/utils/task_runner.py +69 -22
- ivoryos/utils/utils.py +48 -5
- ivoryos/version.py +1 -1
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/METADATA +109 -47
- ivoryos-1.4.4.dist-info/RECORD +119 -0
- ivoryos-1.4.4.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/conftest.py +133 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_route_auth.py +80 -0
- tests/integration/test_route_control.py +94 -0
- tests/integration/test_route_database.py +61 -0
- tests/integration/test_route_design.py +36 -0
- tests/integration/test_route_main.py +35 -0
- tests/integration/test_sockets.py +26 -0
- tests/unit/test_type_conversion.py +42 -0
- tests/unit/test_util.py +3 -0
- ivoryos/routes/api/api.py +0 -56
- ivoryos-1.2.5.dist-info/RECORD +0 -100
- ivoryos-1.2.5.dist-info/top_level.txt +0 -1
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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>
|