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
|
@@ -4,127 +4,357 @@
|
|
|
4
4
|
|
|
5
5
|
{% block body %}
|
|
6
6
|
<style>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
.vis-time-axis .vis-text.vis-minor,
|
|
8
|
+
.vis-time-axis .vis-text.vis-major {
|
|
9
|
+
color: #666;
|
|
10
|
+
}
|
|
11
|
+
.vis-item.stop {
|
|
12
|
+
background-color: #dc3545;
|
|
13
|
+
color: white;
|
|
14
|
+
border: none;
|
|
15
|
+
font-weight: bold;
|
|
16
|
+
}
|
|
17
|
+
.vis-item.prep {
|
|
18
|
+
background-color: #17a2b8;
|
|
19
|
+
border-color: #138496;
|
|
20
|
+
}
|
|
21
|
+
.vis-item.script {
|
|
22
|
+
background-color: #28a745;
|
|
23
|
+
border-color: #1e7e34;
|
|
24
|
+
}
|
|
25
|
+
.vis-item.cleanup {
|
|
26
|
+
background-color: #ffc107;
|
|
27
|
+
border-color: #d39e00;
|
|
28
|
+
color: #212529;
|
|
29
|
+
}
|
|
30
|
+
#visualization {
|
|
31
|
+
border: 1px solid #dee2e6;
|
|
32
|
+
border-radius: 0.375rem;
|
|
33
|
+
background-color: #fff;
|
|
34
|
+
min-height: 200px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.section-header {
|
|
38
|
+
border-bottom: 2px solid #dee2e6;
|
|
39
|
+
padding-bottom: 0.5rem;
|
|
40
|
+
margin-bottom: 1rem;
|
|
41
|
+
color: #495057;
|
|
42
|
+
}
|
|
43
|
+
.loading-spinner {
|
|
44
|
+
display: none;
|
|
45
|
+
}
|
|
17
46
|
</style>
|
|
18
47
|
|
|
19
|
-
<div id="timeline"></div>
|
|
20
48
|
|
|
49
|
+
<div class="timeline-section" style="margin-bottom: 2rem;">
|
|
50
|
+
<h3 class="section-header">
|
|
51
|
+
<i class="fas fa-clock me-2"></i>Execution Timeline
|
|
52
|
+
</h3>
|
|
53
|
+
<div class="alert alert-info" role="alert">
|
|
54
|
+
<i class="fas fa-info-circle me-2"></i>
|
|
55
|
+
<strong>Tip:</strong> Click on timeline items to navigate to detailed views. Use Ctrl+scroll to zoom.
|
|
56
|
+
</div>
|
|
57
|
+
<div id="visualization"></div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- Phase Output Plot Section -->
|
|
61
|
+
|
|
62
|
+
<div class="data-section" style="margin-bottom: 2rem;">
|
|
63
|
+
<h3 class="section-header">
|
|
64
|
+
<div class="col-md-6">
|
|
65
|
+
<label for="output-select" class="form-label">Select Data Type:</label>
|
|
66
|
+
<select id="output-select" class="form-select">
|
|
67
|
+
<option value="">Loading data types...</option>
|
|
68
|
+
</select>
|
|
69
|
+
</div>
|
|
70
|
+
</h3>
|
|
71
|
+
|
|
72
|
+
<div class="plot-controls">
|
|
73
|
+
<div>
|
|
74
|
+
<div class="col-md-6 text-md-end">
|
|
75
|
+
<div class="loading-spinner">
|
|
76
|
+
<div class="spinner-border spinner-border-sm text-primary me-2" role="status">
|
|
77
|
+
<span class="visually-hidden">Loading...</span>
|
|
78
|
+
</div>
|
|
79
|
+
Loading plot data...
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div id="phase-plot" style="height:450px;" class="border rounded bg-white shadow-sm"></div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Workflow Details Section -->
|
|
88
|
+
|
|
89
|
+
<div>
|
|
90
|
+
<h2 class="section-header">
|
|
91
|
+
<i class="fas fa-project-diagram me-2"></i>Workflow: {{ workflow.name }}
|
|
92
|
+
</h2>
|
|
93
|
+
|
|
94
|
+
<!-- Prep Phase -->
|
|
95
|
+
{% if grouped.prep %}
|
|
96
|
+
<div class="mb-4">
|
|
97
|
+
<h4 class="text-info mb-3">
|
|
98
|
+
<i class="fas fa-tools me-2"></i>Prep
|
|
99
|
+
</h4>
|
|
100
|
+
<div class="row">
|
|
101
|
+
{% for phase in grouped.prep %}
|
|
102
|
+
<div class="col-lg-6 mb-3">
|
|
103
|
+
{% include "components/step_card.html" %}
|
|
104
|
+
</div>
|
|
105
|
+
{% endfor %}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
{% endif %}
|
|
109
|
+
|
|
110
|
+
<!-- Script Iterations -->
|
|
111
|
+
{% for repeat_index, phase_list in grouped.script.items()|sort %}
|
|
112
|
+
<div class="mb-4" id="card-iter{{ repeat_index }}">
|
|
113
|
+
<h4 class="text-success mb-3">
|
|
114
|
+
<i class="fas fa-redo me-2"></i>Iteration {{ repeat_index }}
|
|
115
|
+
</h4>
|
|
116
|
+
<div class="row">
|
|
117
|
+
{% for phase in phase_list %}
|
|
118
|
+
<div class="col-lg-6 mb-3">
|
|
119
|
+
{% include "components/step_card.html" %}
|
|
120
|
+
</div>
|
|
121
|
+
{% endfor %}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
{% endfor %}
|
|
125
|
+
|
|
126
|
+
<!-- Cleanup Phase -->
|
|
127
|
+
{% if grouped.cleanup %}
|
|
128
|
+
<div>
|
|
129
|
+
<h4 class="text-warning mb-3">
|
|
130
|
+
<i class="fas fa-broom me-2"></i>Cleanup
|
|
131
|
+
</h4>
|
|
132
|
+
<div class="row">
|
|
133
|
+
{% for phase in grouped.cleanup %}
|
|
134
|
+
<div class="col-lg-6 mb-3">
|
|
135
|
+
{% include "components/step_card.html" %}
|
|
136
|
+
</div>
|
|
137
|
+
{% endfor %}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
{% endif %}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
<!-- External Dependencies -->
|
|
21
146
|
<script src="https://unpkg.com/vis-timeline@latest/standalone/umd/vis-timeline-graph2d.min.js"></script>
|
|
22
147
|
<link href="https://unpkg.com/vis-timeline@latest/styles/vis-timeline-graph2d.min.css" rel="stylesheet"/>
|
|
148
|
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
|
23
149
|
|
|
24
|
-
<
|
|
150
|
+
<script type="text/javascript">
|
|
151
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
152
|
+
// ---------------- Timeline Setup ----------------
|
|
153
|
+
const container = document.getElementById('visualization');
|
|
154
|
+
const groups = [
|
|
155
|
+
{ id: 'all', content: 'Workflow Execution' }
|
|
156
|
+
];
|
|
25
157
|
|
|
26
|
-
|
|
158
|
+
const items = [
|
|
159
|
+
{% if grouped.prep %}
|
|
160
|
+
{
|
|
161
|
+
id: 'prep',
|
|
162
|
+
content: 'Prep',
|
|
163
|
+
start: '{{ grouped.prep[0].start_time }}',
|
|
164
|
+
end: '{{ grouped.prep[-1].end_time }}',
|
|
165
|
+
className: 'prep',
|
|
166
|
+
group: 'all',
|
|
167
|
+
},
|
|
168
|
+
{% endif %}
|
|
27
169
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
170
|
+
{% for repeat_index, step_list in grouped.script.items()|sort %}
|
|
171
|
+
{
|
|
172
|
+
id: 'iter{{ repeat_index }}',
|
|
173
|
+
content: 'Iteration {{ repeat_index }}',
|
|
174
|
+
start: '{{ step_list[0].start_time }}',
|
|
175
|
+
end: '{{ step_list[-1].end_time }}',
|
|
176
|
+
className: 'script',
|
|
177
|
+
group: 'all',
|
|
178
|
+
},
|
|
179
|
+
{% for step in step_list %}
|
|
180
|
+
{% if step.method_name == "stop" %}
|
|
181
|
+
{
|
|
182
|
+
id: 'stop-{{ step.id }}',
|
|
183
|
+
content: '🛑 Stop',
|
|
184
|
+
start: '{{ step.start_time }}',
|
|
185
|
+
type: 'point',
|
|
186
|
+
className: 'stop',
|
|
187
|
+
group: 'all',
|
|
188
|
+
title: 'Stop event at {{ step.start_time }}'
|
|
189
|
+
},
|
|
190
|
+
{% endif %}
|
|
191
|
+
{% endfor %}
|
|
192
|
+
{% endfor %}
|
|
193
|
+
|
|
194
|
+
{% if grouped.cleanup %}
|
|
195
|
+
{
|
|
196
|
+
id: 'cleanup',
|
|
197
|
+
content: 'Cleanup ',
|
|
198
|
+
start: '{{ grouped.cleanup[0].start_time }}',
|
|
199
|
+
end: '{{ grouped.cleanup[-1].end_time }}',
|
|
200
|
+
className: 'cleanup',
|
|
201
|
+
group: 'all',
|
|
202
|
+
},
|
|
203
|
+
{% endif %}
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
var timeline = new vis.Timeline(container, items, groups, {
|
|
207
|
+
clickToUse: true,
|
|
208
|
+
stack: false, // keep items from overlapping vertically
|
|
209
|
+
horizontalScroll: true,
|
|
210
|
+
zoomKey: 'ctrlKey'
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
timeline.on('select', function (props) {
|
|
214
|
+
const id = props.items[0];
|
|
215
|
+
if (id && id.startsWith('iter')) {
|
|
216
|
+
const card = document.getElementById('card-' + id);
|
|
217
|
+
if (card) {
|
|
218
|
+
const yOffset = -80;
|
|
219
|
+
const y = card.getBoundingClientRect().top + window.pageYOffset + yOffset;
|
|
220
|
+
window.scrollTo({ top: y, behavior: 'smooth' });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ---------------- Phase Data Plot ----------------
|
|
226
|
+
const loadingSpinner = document.querySelector('.loading-spinner');
|
|
227
|
+
const select = document.getElementById('output-select');
|
|
228
|
+
|
|
229
|
+
loadingSpinner.style.display = 'block';
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
fetch("{{ url_for('data.workflow_phase_data', workflow_id=workflow.id) }}")
|
|
233
|
+
.then(res => {
|
|
234
|
+
if (!res.ok) {
|
|
235
|
+
throw new Error(`HTTP error! status: ${res.status}`);
|
|
236
|
+
}
|
|
237
|
+
return res.json();
|
|
238
|
+
})
|
|
239
|
+
.then(data => {
|
|
240
|
+
loadingSpinner.style.display = 'none';
|
|
241
|
+
|
|
242
|
+
const repeatKeys = Object.keys(data).sort((a, b) => a - b);
|
|
243
|
+
const dataSection = document.querySelector('.data-section'); // Get the entire section
|
|
244
|
+
|
|
245
|
+
if (!repeatKeys.length) {
|
|
246
|
+
// Hide the entire data section if no data
|
|
247
|
+
dataSection.style.display = 'none';
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const allKeys = new Set();
|
|
252
|
+
repeatKeys.forEach(k => {
|
|
253
|
+
Object.keys(data[k]).forEach(key => allKeys.add(key));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// If no keys found, also hide the section
|
|
257
|
+
if (allKeys.size === 0) {
|
|
258
|
+
dataSection.style.display = 'none';
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Show the data section since we have data
|
|
263
|
+
dataSection.style.display = 'block';
|
|
264
|
+
|
|
265
|
+
// Clear and populate select options
|
|
266
|
+
select.innerHTML = '';
|
|
267
|
+
allKeys.forEach(k => {
|
|
268
|
+
const option = new Option(k, k);
|
|
269
|
+
select.appendChild(option);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
function plotData(selectedKey) {
|
|
273
|
+
const x = [];
|
|
274
|
+
const y = [];
|
|
275
|
+
|
|
276
|
+
repeatKeys.forEach(repeat_index => {
|
|
277
|
+
const arr = data[repeat_index][selectedKey];
|
|
278
|
+
if (arr && arr.length) {
|
|
279
|
+
const batchCount = arr.length;
|
|
280
|
+
|
|
281
|
+
arr.forEach((d, batchIdx) => {
|
|
282
|
+
// Compute fractional x for batch indexing: e.g. 1.1, 1.2, 1.3
|
|
283
|
+
const xVal = parseFloat(repeat_index) + (batchIdx + 1) / (batchCount + 1);
|
|
284
|
+
|
|
285
|
+
if (typeof d === 'object' && d.x !== undefined && d.y !== undefined) {
|
|
286
|
+
x.push(xVal);
|
|
287
|
+
y.push(d.y);
|
|
288
|
+
} else if (typeof d === 'number') {
|
|
289
|
+
x.push(xVal);
|
|
290
|
+
y.push(d);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const trace = {
|
|
297
|
+
x: x,
|
|
298
|
+
y: y,
|
|
299
|
+
mode: 'markers',
|
|
300
|
+
name: selectedKey,
|
|
301
|
+
marker: { size: 8 },
|
|
302
|
+
line: { shape: 'linear', width: 1 },
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const layout = {
|
|
306
|
+
xaxis: {
|
|
307
|
+
title: 'Trial (Batch Sub-index)',
|
|
308
|
+
gridcolor: '#e9ecef',
|
|
309
|
+
tickvals: repeatKeys.map(k => parseFloat(k)),
|
|
310
|
+
ticktext: repeatKeys.map(k => `Trial ${k}`),
|
|
311
|
+
},
|
|
312
|
+
yaxis: {
|
|
313
|
+
title: selectedKey,
|
|
314
|
+
gridcolor: '#e9ecef'
|
|
315
|
+
},
|
|
316
|
+
plot_bgcolor: '#ffffff',
|
|
317
|
+
paper_bgcolor: '#ffffff',
|
|
318
|
+
margin: { t: 60, r: 40, b: 60, l: 80 }
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const config = {
|
|
322
|
+
responsive: true,
|
|
323
|
+
displayModeBar: true,
|
|
324
|
+
modeBarButtonsToRemove: ['lasso2d', 'select2d']
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
Plotly.newPlot('phase-plot', [trace], layout, config);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
select.addEventListener('change', e => {
|
|
331
|
+
if (e.target.value) {
|
|
332
|
+
plotData(e.target.value);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Plot first available data type
|
|
337
|
+
if (allKeys.size > 0) {
|
|
338
|
+
plotData([...allKeys][0]);
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
.catch(error => {
|
|
342
|
+
loadingSpinner.style.display = 'none';
|
|
343
|
+
console.error('Error loading phase data:', error);
|
|
344
|
+
|
|
345
|
+
const dataSection = document.querySelector('.data-section');
|
|
346
|
+
// Hide the section on error as well
|
|
347
|
+
dataSection.style.display = 'none';
|
|
348
|
+
|
|
349
|
+
// Optionally, you could show an error message instead:
|
|
350
|
+
// dataSection.innerHTML = `
|
|
351
|
+
// <div class="alert alert-danger m-3" role="alert">
|
|
352
|
+
// <i class="fas fa-exclamation-triangle me-2"></i>
|
|
353
|
+
// <strong>Error:</strong> Unable to load phase data. ${error.message}
|
|
354
|
+
// </div>
|
|
355
|
+
// `;
|
|
356
|
+
});
|
|
357
|
+
});
|
|
106
358
|
</script>
|
|
107
359
|
|
|
108
|
-
<h2>Workflow: {{ workflow.name }}</h2>
|
|
109
|
-
|
|
110
|
-
{% if grouped.prep %}
|
|
111
|
-
<h4 class="mt-4">Prep Phase</h4>
|
|
112
|
-
{% for step in grouped.prep %}
|
|
113
|
-
{% include "components/step_card.html" %}
|
|
114
|
-
{% endfor %}
|
|
115
|
-
{% endif %}
|
|
116
|
-
|
|
117
|
-
{% for repeat_index, step_list in grouped.script.items()|sort %}
|
|
118
|
-
<h4 class="mt-4" id="card-iter{{ repeat_index }}">Iteration {{ repeat_index }}</h4>
|
|
119
|
-
{% for step in step_list %}
|
|
120
|
-
{% include "components/step_card.html" %}
|
|
121
|
-
{% endfor %}
|
|
122
|
-
{% endfor %}
|
|
123
|
-
|
|
124
|
-
{% if grouped.cleanup %}
|
|
125
|
-
<h4 class="mt-4">Cleanup Phase</h4>
|
|
126
|
-
{% for step in grouped.cleanup %}
|
|
127
|
-
{% include "components/step_card.html" %}
|
|
128
|
-
{% endfor %}
|
|
129
|
-
{% endif %}
|
|
130
360
|
{% endblock %}
|
ivoryos/routes/design/design.py
CHANGED
|
@@ -35,6 +35,9 @@ def _create_forms(instrument, script, autofill, pseudo_deck = None):
|
|
|
35
35
|
_object = global_config.defined_variables.get(instrument)
|
|
36
36
|
functions = utils._inspect_class(_object)
|
|
37
37
|
forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
|
|
38
|
+
elif instrument.startswith("blocks"):
|
|
39
|
+
forms = create_form_from_pseudo(pseudo=global_config.building_blocks[instrument], autofill=autofill, script=script)
|
|
40
|
+
functions = global_config.building_blocks[instrument]
|
|
38
41
|
else:
|
|
39
42
|
if deck:
|
|
40
43
|
functions = global_config.deck_snapshot.get(instrument, {})
|
|
@@ -84,15 +87,39 @@ def experiment_builder():
|
|
|
84
87
|
|
|
85
88
|
# edit_action_info = session.get("edit_action")
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
try:
|
|
91
|
+
exec_string = script.python_script if script.python_script else script.compile(current_app.config['SCRIPT_FOLDER'])
|
|
92
|
+
except Exception as e:
|
|
93
|
+
exec_string = {}
|
|
94
|
+
flash(f"Error in Python script: {e}")
|
|
89
95
|
session['python_code'] = exec_string
|
|
90
96
|
|
|
91
97
|
design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
|
|
92
98
|
|
|
93
99
|
return render_template('experiment_builder.html', off_line=off_line, history=deck_list,
|
|
94
100
|
script=script, defined_variables=deck_variables, buttons_dict=design_buttons,
|
|
95
|
-
local_variables=global_config.defined_variables)
|
|
101
|
+
local_variables=global_config.defined_variables, block_variables=global_config.building_blocks)
|
|
102
|
+
|
|
103
|
+
@design.route("/draft/code_preview", methods=["GET"])
|
|
104
|
+
@login_required
|
|
105
|
+
def compile_preview():
|
|
106
|
+
# Get mode and batch from query parameters
|
|
107
|
+
script = utils.get_script_file()
|
|
108
|
+
mode = request.args.get("mode", "single") # default to "single"
|
|
109
|
+
batch = request.args.get("batch", "sample") # default to "sample"
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
# Example: decide which code to return based on mode/batch
|
|
113
|
+
if mode == "single":
|
|
114
|
+
code = script.compile(current_app.config['SCRIPT_FOLDER'])
|
|
115
|
+
elif mode == "batch":
|
|
116
|
+
code = script.compile(current_app.config['SCRIPT_FOLDER'], batch=True, mode=batch)
|
|
117
|
+
else:
|
|
118
|
+
code = "Invalid mode. Please select 'single' or 'batch'."
|
|
119
|
+
except Exception as e:
|
|
120
|
+
code = f"Error compiling: {e}"
|
|
121
|
+
# print(code)
|
|
122
|
+
return jsonify(code=code)
|
|
96
123
|
|
|
97
124
|
|
|
98
125
|
@design.route("/draft/meta", methods=["PATCH"])
|
|
@@ -192,7 +219,8 @@ def update_ui_state():
|
|
|
192
219
|
deck_variables = list(pseudo_deck.keys()) if pseudo_deck else []
|
|
193
220
|
deck_variables.remove("deck_name") if len(deck_variables) > 0 else deck_variables
|
|
194
221
|
html = render_template("components/sidebar.html", history=deck_list,
|
|
195
|
-
defined_variables=deck_variables, local_variables = global_config.defined_variables
|
|
222
|
+
defined_variables=deck_variables, local_variables = global_config.defined_variables,
|
|
223
|
+
block_variables=global_config.building_blocks)
|
|
196
224
|
return jsonify({"html": html})
|
|
197
225
|
return jsonify({"error": "Invalid request"}), 400
|
|
198
226
|
|
|
@@ -253,7 +281,7 @@ def submit_script():
|
|
|
253
281
|
"""
|
|
254
282
|
.. :quickref: Workflow Design; convert Python to workflow script
|
|
255
283
|
|
|
256
|
-
.. http:post:: /
|
|
284
|
+
.. http:post:: /draft/submit_python
|
|
257
285
|
|
|
258
286
|
Convert a Python script to a workflow script and save it in the database.
|
|
259
287
|
|
|
@@ -310,26 +338,39 @@ def methods_handler(instrument: str = ''):
|
|
|
310
338
|
|
|
311
339
|
success = True
|
|
312
340
|
msg = ""
|
|
341
|
+
request.form
|
|
313
342
|
if "hidden_name" in request.form:
|
|
343
|
+
deck_snapshot = global_config.deck_snapshot
|
|
344
|
+
block_snapshot = global_config.building_blocks
|
|
314
345
|
method_name = request.form.get("hidden_name", None)
|
|
315
346
|
form = forms.get(method_name) if forms else None
|
|
316
347
|
insert_position = request.form.get("drop_target_id", None)
|
|
348
|
+
|
|
317
349
|
if form:
|
|
318
350
|
kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
|
|
319
351
|
if form.validate_on_submit():
|
|
320
352
|
function_name = kwargs.pop("hidden_name")
|
|
353
|
+
batch_action = kwargs.pop("batch_action", False)
|
|
321
354
|
save_data = kwargs.pop('return', '')
|
|
322
355
|
primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
|
|
323
356
|
|
|
324
357
|
# todo
|
|
325
|
-
print(primitive_arg_types)
|
|
358
|
+
# print(primitive_arg_types)
|
|
326
359
|
|
|
327
360
|
script.eval_list(kwargs, primitive_arg_types)
|
|
328
361
|
kwargs = script.validate_variables(kwargs)
|
|
362
|
+
coroutine = False
|
|
363
|
+
if instrument.startswith("deck") and deck_snapshot:
|
|
364
|
+
coroutine = deck_snapshot[instrument][function_name].get("coroutine", False)
|
|
365
|
+
elif instrument.startswith("blocks") and block_snapshot:
|
|
366
|
+
coroutine = block_snapshot[instrument][function_name].get("coroutine", False)
|
|
329
367
|
action = {"instrument": instrument, "action": function_name,
|
|
330
368
|
"args": kwargs,
|
|
331
369
|
"return": save_data,
|
|
332
|
-
'arg_types': primitive_arg_types
|
|
370
|
+
'arg_types': primitive_arg_types,
|
|
371
|
+
"coroutine": coroutine,
|
|
372
|
+
"batch_action": batch_action,
|
|
373
|
+
}
|
|
333
374
|
script.add_action(action=action, insert_position=insert_position)
|
|
334
375
|
else:
|
|
335
376
|
msg = [f"{field}: {', '.join(messages)}" for field, messages in form.errors.items()]
|
|
@@ -374,7 +415,13 @@ def methods_handler(instrument: str = ''):
|
|
|
374
415
|
success = False
|
|
375
416
|
msg = [f"{field}: {', '.join(messages)}" for field, messages in form.errors.items()]
|
|
376
417
|
utils.post_script_file(script)
|
|
377
|
-
|
|
418
|
+
#TODO
|
|
419
|
+
try:
|
|
420
|
+
exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
|
|
421
|
+
except Exception as e:
|
|
422
|
+
exec_string = {}
|
|
423
|
+
msg = f"Compilation failed: {str(e)}"
|
|
424
|
+
# exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
|
|
378
425
|
session['python_code'] = exec_string
|
|
379
426
|
design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
|
|
380
427
|
html = render_template("components/canvas_main.html", script=script, buttons_dict=design_buttons)
|
|
@@ -388,7 +435,7 @@ def get_operation_sidebar(instrument: str = ''):
|
|
|
388
435
|
"""
|
|
389
436
|
.. :quickref: Workflow Design; handle methods of a specific instrument
|
|
390
437
|
|
|
391
|
-
.. http:get:: /
|
|
438
|
+
.. http:get:: /draft/instruments/<string:instrument>
|
|
392
439
|
|
|
393
440
|
:param instrument: The name of the instrument to handle methods for.
|
|
394
441
|
:type instrument: str
|
|
@@ -422,7 +469,9 @@ def get_operation_sidebar(instrument: str = ''):
|
|
|
422
469
|
# edit_action_info = session.get("edit_action")
|
|
423
470
|
html = render_template("components/sidebar.html", off_line=off_line, history=deck_list,
|
|
424
471
|
defined_variables=deck_variables,
|
|
425
|
-
local_variables=global_config.defined_variables
|
|
472
|
+
local_variables=global_config.defined_variables,
|
|
473
|
+
block_variables=global_config.building_blocks,
|
|
474
|
+
)
|
|
426
475
|
return jsonify({"html": html})
|
|
427
476
|
|
|
428
477
|
|
|
@@ -17,7 +17,7 @@ def load_json():
|
|
|
17
17
|
.. http:post:: /files/script-json
|
|
18
18
|
|
|
19
19
|
:form file: workflow design JSON file
|
|
20
|
-
:status 302: load script json and then redirects to :http:get:`/ivoryos/
|
|
20
|
+
:status 302: load script json and then redirects to :http:get:`/ivoryos/draft`
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
if request.method == "POST":
|
|
@@ -38,7 +38,7 @@ def download_python():
|
|
|
38
38
|
|
|
39
39
|
.. http:post:: /files/script-python
|
|
40
40
|
|
|
41
|
-
:status 302: redirects to :http:get:`/ivoryos/
|
|
41
|
+
:status 302: redirects to :http:get:`/ivoryos/draft`
|
|
42
42
|
"""
|
|
43
43
|
script = utils.get_script_file()
|
|
44
44
|
run_name = script.name if script.name else "untitled"
|
|
@@ -53,7 +53,7 @@ def download_json():
|
|
|
53
53
|
|
|
54
54
|
.. http:post:: /files/script-json
|
|
55
55
|
|
|
56
|
-
:status 302: redirects to :http:get:`/ivoryos/
|
|
56
|
+
:status 302: redirects to :http:get:`/ivoryos/draft`
|
|
57
57
|
"""
|
|
58
58
|
script = utils.get_script_file()
|
|
59
59
|
run_name = script.name if script.name else "untitled"
|