ivoryos 1.0.9__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 +17 -207
- ivoryos/app.py +154 -0
- ivoryos/config.py +1 -0
- ivoryos/optimizer/ax_optimizer.py +191 -0
- ivoryos/optimizer/base_optimizer.py +84 -0
- ivoryos/optimizer/baybe_optimizer.py +193 -0
- ivoryos/optimizer/nimo_optimizer.py +173 -0
- ivoryos/optimizer/registry.py +11 -0
- ivoryos/routes/auth/auth.py +43 -14
- ivoryos/routes/auth/templates/change_password.html +32 -0
- ivoryos/routes/control/control.py +101 -366
- ivoryos/routes/control/control_file.py +33 -0
- ivoryos/routes/control/control_new_device.py +152 -0
- ivoryos/routes/control/templates/controllers.html +193 -0
- ivoryos/routes/control/templates/controllers_new.html +112 -0
- ivoryos/routes/control/utils.py +40 -0
- ivoryos/routes/data/data.py +197 -0
- ivoryos/routes/data/templates/components/step_card.html +78 -0
- ivoryos/routes/{database/templates/database → data/templates}/workflow_database.html +14 -8
- ivoryos/routes/data/templates/workflow_view.html +360 -0
- ivoryos/routes/design/__init__.py +4 -0
- ivoryos/routes/design/design.py +348 -657
- ivoryos/routes/design/design_file.py +68 -0
- ivoryos/routes/design/design_step.py +171 -0
- ivoryos/routes/design/templates/components/action_form.html +53 -0
- ivoryos/routes/design/templates/components/actions_panel.html +25 -0
- ivoryos/routes/design/templates/components/autofill_toggle.html +10 -0
- ivoryos/routes/design/templates/components/canvas.html +5 -0
- ivoryos/routes/design/templates/components/canvas_footer.html +9 -0
- ivoryos/routes/design/templates/components/canvas_header.html +75 -0
- ivoryos/routes/design/templates/components/canvas_main.html +39 -0
- ivoryos/routes/design/templates/components/deck_selector.html +10 -0
- ivoryos/routes/design/templates/components/edit_action_form.html +53 -0
- ivoryos/routes/design/templates/components/info_modal.html +318 -0
- ivoryos/routes/design/templates/components/instruments_panel.html +88 -0
- ivoryos/routes/design/templates/components/modals/drop_modal.html +17 -0
- ivoryos/routes/design/templates/components/modals/json_modal.html +22 -0
- ivoryos/routes/design/templates/components/modals/new_script_modal.html +17 -0
- ivoryos/routes/design/templates/components/modals/rename_modal.html +23 -0
- ivoryos/routes/design/templates/components/modals/saveas_modal.html +27 -0
- ivoryos/routes/design/templates/components/modals.html +6 -0
- ivoryos/routes/design/templates/components/python_code_overlay.html +56 -0
- ivoryos/routes/design/templates/components/sidebar.html +15 -0
- ivoryos/routes/design/templates/components/text_to_code_panel.html +20 -0
- ivoryos/routes/design/templates/experiment_builder.html +44 -0
- ivoryos/routes/execute/__init__.py +0 -0
- ivoryos/routes/execute/execute.py +377 -0
- ivoryos/routes/execute/execute_file.py +78 -0
- ivoryos/routes/execute/templates/components/error_modal.html +20 -0
- ivoryos/routes/execute/templates/components/logging_panel.html +56 -0
- ivoryos/routes/execute/templates/components/progress_panel.html +27 -0
- ivoryos/routes/execute/templates/components/run_panel.html +9 -0
- ivoryos/routes/execute/templates/components/run_tabs.html +60 -0
- ivoryos/routes/execute/templates/components/tab_bayesian.html +520 -0
- ivoryos/routes/execute/templates/components/tab_configuration.html +383 -0
- ivoryos/routes/execute/templates/components/tab_repeat.html +18 -0
- ivoryos/routes/execute/templates/experiment_run.html +30 -0
- ivoryos/routes/library/__init__.py +0 -0
- ivoryos/routes/library/library.py +157 -0
- ivoryos/routes/{database/templates/database/scripts_database.html → library/templates/library.html} +32 -23
- ivoryos/routes/main/main.py +31 -3
- ivoryos/routes/main/templates/{main/home.html → home.html} +4 -4
- ivoryos/server.py +180 -0
- ivoryos/socket_handlers.py +52 -0
- ivoryos/static/ivoryos_logo.png +0 -0
- ivoryos/static/js/action_handlers.js +384 -0
- ivoryos/static/js/db_delete.js +23 -0
- ivoryos/static/js/script_metadata.js +39 -0
- ivoryos/static/js/socket_handler.js +40 -5
- ivoryos/static/js/sortable_design.js +107 -56
- ivoryos/static/js/ui_state.js +114 -0
- ivoryos/templates/base.html +67 -8
- ivoryos/utils/bo_campaign.py +180 -3
- ivoryos/utils/client_proxy.py +267 -36
- ivoryos/utils/db_models.py +300 -65
- ivoryos/utils/decorators.py +34 -0
- ivoryos/utils/form.py +63 -29
- ivoryos/utils/global_config.py +34 -1
- ivoryos/utils/nest_script.py +314 -0
- ivoryos/utils/py_to_json.py +295 -0
- ivoryos/utils/script_runner.py +599 -165
- ivoryos/utils/serilize.py +201 -0
- ivoryos/utils/task_runner.py +71 -21
- ivoryos/utils/utils.py +50 -6
- ivoryos/version.py +1 -1
- ivoryos-1.4.4.dist-info/METADATA +263 -0
- ivoryos-1.4.4.dist-info/RECORD +119 -0
- {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +1 -1
- {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/top_level.txt +1 -0
- tests/unit/test_type_conversion.py +42 -0
- tests/unit/test_util.py +3 -0
- ivoryos/routes/control/templates/control/controllers.html +0 -78
- ivoryos/routes/control/templates/control/controllers_home.html +0 -55
- ivoryos/routes/control/templates/control/controllers_new.html +0 -89
- ivoryos/routes/database/database.py +0 -306
- ivoryos/routes/database/templates/database/step_card.html +0 -7
- ivoryos/routes/database/templates/database/workflow_view.html +0 -130
- ivoryos/routes/design/templates/design/experiment_builder.html +0 -521
- ivoryos/routes/design/templates/design/experiment_run.html +0 -558
- ivoryos-1.0.9.dist-info/METADATA +0 -218
- ivoryos-1.0.9.dist-info/RECORD +0 -61
- /ivoryos/routes/auth/templates/{auth/login.html → login.html} +0 -0
- /ivoryos/routes/auth/templates/{auth/signup.html → signup.html} +0 -0
- /ivoryos/routes/{database → data}/__init__.py +0 -0
- /ivoryos/routes/main/templates/{main/help.html → help.html} +0 -0
- {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{% extends 'base.html' %}
|
|
2
|
+
{% block title %}IvoryOS | Design{% endblock %}
|
|
3
|
+
|
|
4
|
+
{% block body %}
|
|
5
|
+
{# overlay block for text-to-code gen #}
|
|
6
|
+
|
|
7
|
+
<div id="overlay" class="overlay">
|
|
8
|
+
<div>
|
|
9
|
+
<h3 id="overlay-text">Generating design, please wait...</h3>
|
|
10
|
+
<div class="spinner-border" role="status"></div>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="row">
|
|
15
|
+
<div class="col-md-3 scroll-column" id="sidebar-wrapper">
|
|
16
|
+
{% include 'components/sidebar.html' %}
|
|
17
|
+
</div>
|
|
18
|
+
<div class="col-md-9 scroll-column" id="canvas-wrapper">
|
|
19
|
+
{% include 'components/canvas.html' %}
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
{# Include all modals #}
|
|
25
|
+
{% include 'components/modals.html' %}
|
|
26
|
+
{% include 'components/info_modal.html' %}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
{# Include all scripts #}
|
|
30
|
+
<script>
|
|
31
|
+
const updateListUrl = "{{ url_for('design.design_steps.update_list') }}";
|
|
32
|
+
const scriptUIStateUrl = "{{ url_for('design.update_ui_state') }}";
|
|
33
|
+
const scriptMetaUrl = "{{ url_for('design.update_script_meta') }}";
|
|
34
|
+
const scriptStepUrl = `{{ url_for('design.design_steps.get_step', uuid=0) }}`;
|
|
35
|
+
const scriptStepDupUrl = `{{ url_for('design.design_steps.duplicate_action', uuid=0) }}`;
|
|
36
|
+
const scriptDeleteUrl = "{{ url_for('design.clear_draft') }}";
|
|
37
|
+
const scriptCompileUrl = "{{ url_for('design.compile_preview') }}";
|
|
38
|
+
</script>
|
|
39
|
+
<script src="{{ url_for('static', filename='js/sortable_design.js') }}"></script>
|
|
40
|
+
<script src="{{ url_for('static', filename='js/action_handlers.js') }}"></script>
|
|
41
|
+
<script src="{{ url_for('static', filename='js/script_metadata.js') }}"></script>
|
|
42
|
+
<script src="{{ url_for('static', filename='js/ui_state.js') }}"></script>
|
|
43
|
+
|
|
44
|
+
{% endblock %}
|
|
File without changes
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from flask import Blueprint, redirect, url_for, flash, jsonify, request, render_template, session, \
|
|
6
|
+
current_app, g, send_file
|
|
7
|
+
from flask_login import login_required
|
|
8
|
+
|
|
9
|
+
from ivoryos.routes.execute.execute_file import files
|
|
10
|
+
from ivoryos.utils import utils
|
|
11
|
+
from ivoryos.utils.bo_campaign import parse_optimization_form
|
|
12
|
+
from ivoryos.utils.db_models import SingleStep, WorkflowRun, WorkflowStep, WorkflowPhase
|
|
13
|
+
from ivoryos.utils.global_config import GlobalConfig
|
|
14
|
+
from ivoryos.utils.form import create_action_button
|
|
15
|
+
|
|
16
|
+
from werkzeug.utils import secure_filename
|
|
17
|
+
|
|
18
|
+
from ivoryos.socket_handlers import runner, retry, pause, abort_pending, abort_current
|
|
19
|
+
|
|
20
|
+
execute = Blueprint('execute', __name__, template_folder='templates')
|
|
21
|
+
|
|
22
|
+
execute.register_blueprint(files)
|
|
23
|
+
# Register sub-blueprints
|
|
24
|
+
global_config = GlobalConfig()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@execute.route("/executions/config", methods=['GET', 'POST'])
|
|
28
|
+
@login_required
|
|
29
|
+
def experiment_run():
|
|
30
|
+
"""
|
|
31
|
+
.. :quickref: Workflow Execution Config; Execute/iterate the workflow
|
|
32
|
+
|
|
33
|
+
.. http:get:: /executions/config
|
|
34
|
+
|
|
35
|
+
Load the experiment execution interface.
|
|
36
|
+
|
|
37
|
+
.. http:post:: /executions/config
|
|
38
|
+
|
|
39
|
+
Start workflow execution with experiment configuration.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
deck = global_config.deck
|
|
43
|
+
script = utils.get_script_file()
|
|
44
|
+
# runner = global_config.runner
|
|
45
|
+
existing_data = None
|
|
46
|
+
# script.sort_actions() # handled in update list
|
|
47
|
+
off_line = current_app.config["OFF_LINE"]
|
|
48
|
+
deck_list = utils.import_history(os.path.join(current_app.config["OUTPUT_FOLDER"], 'deck_history.txt'))
|
|
49
|
+
optimizers_schema = {k: v.get_schema() for k, v in global_config.optimizers.items()}
|
|
50
|
+
design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
|
|
51
|
+
config_preview = []
|
|
52
|
+
config_file_list = [i for i in os.listdir(current_app.config["CSV_FOLDER"]) if not i == ".gitkeep"]
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
exec_string = script.python_script if script.python_script else script.compile(
|
|
56
|
+
current_app.config['SCRIPT_FOLDER'])
|
|
57
|
+
except Exception as e:
|
|
58
|
+
flash(e.__str__())
|
|
59
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
60
|
+
return jsonify({"error": e.__str__()})
|
|
61
|
+
else:
|
|
62
|
+
return redirect(url_for("design.experiment_builder"))
|
|
63
|
+
|
|
64
|
+
config_file = request.args.get("filename")
|
|
65
|
+
config = []
|
|
66
|
+
if config_file:
|
|
67
|
+
session['config_file'] = config_file
|
|
68
|
+
filename = session.get("config_file")
|
|
69
|
+
if filename:
|
|
70
|
+
config = list(csv.DictReader(open(os.path.join(current_app.config['CSV_FOLDER'], filename))))
|
|
71
|
+
config_preview = config[1:]
|
|
72
|
+
arg_type = config.pop(0) # first entry is types
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
# Handle both string and dict exec_string
|
|
76
|
+
if isinstance(exec_string, dict):
|
|
77
|
+
for key, func_str in exec_string.items():
|
|
78
|
+
exec(func_str)
|
|
79
|
+
|
|
80
|
+
else:
|
|
81
|
+
# Handle string case - you might need to adjust this based on your needs
|
|
82
|
+
line_collection = {}
|
|
83
|
+
except Exception:
|
|
84
|
+
flash(f"Please check syntax!!")
|
|
85
|
+
return redirect(url_for("design.experiment_builder"))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
line_collection = script.render_script_lines(script.script_dict)
|
|
89
|
+
|
|
90
|
+
run_name = script.name if script.name else "untitled"
|
|
91
|
+
|
|
92
|
+
dismiss = session.get("dismiss", None)
|
|
93
|
+
script = utils.get_script_file()
|
|
94
|
+
no_deck_warning = False
|
|
95
|
+
|
|
96
|
+
_, return_list = script.config_return()
|
|
97
|
+
config_list, config_type_list = script.config("script")
|
|
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)
|
|
105
|
+
|
|
106
|
+
if deck is None:
|
|
107
|
+
no_deck_warning = True
|
|
108
|
+
flash(f"No deck is found, import {script.deck}")
|
|
109
|
+
elif script.deck:
|
|
110
|
+
is_deck_match = script.deck == deck.__name__ or script.deck == \
|
|
111
|
+
os.path.splitext(os.path.basename(deck.__file__))[0]
|
|
112
|
+
if not is_deck_match:
|
|
113
|
+
flash(f"This script is not compatible with current deck, import {script.deck}")
|
|
114
|
+
|
|
115
|
+
if request.method == "POST":
|
|
116
|
+
# bo_args = None
|
|
117
|
+
compiled = False
|
|
118
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
119
|
+
payload_json = request.get_json()
|
|
120
|
+
compiled = True
|
|
121
|
+
if "kwargs" in payload_json:
|
|
122
|
+
config = payload_json["kwargs"]
|
|
123
|
+
# elif "parameters" in payload_json:
|
|
124
|
+
# bo_args = payload_json
|
|
125
|
+
repeat = payload_json.pop("repeat", None)
|
|
126
|
+
batch_size = payload_json.pop('batch_size', 1)
|
|
127
|
+
else:
|
|
128
|
+
if "bo" in request.form:
|
|
129
|
+
bo_args = request.form.to_dict()
|
|
130
|
+
existing_data = bo_args.pop("existing_data")
|
|
131
|
+
if "online-config" in request.form:
|
|
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))
|
|
136
|
+
repeat = request.form.get('repeat', None)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
# if True:
|
|
140
|
+
datapath = current_app.config["DATA_FOLDER"]
|
|
141
|
+
run_name = script.validate_function_name(run_name)
|
|
142
|
+
runner.run_script(script=script, run_name=run_name, config=config,
|
|
143
|
+
logger=g.logger, socketio=g.socketio, repeat_count=repeat,
|
|
144
|
+
output_path=datapath, compiled=compiled, history=existing_data,
|
|
145
|
+
current_app=current_app._get_current_object(), batch_size=batch_size
|
|
146
|
+
)
|
|
147
|
+
if utils.check_config_duplicate(config):
|
|
148
|
+
flash(f"WARNING: Duplicate in config entries.")
|
|
149
|
+
except Exception as e:
|
|
150
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
151
|
+
return jsonify({"error": e.__str__()})
|
|
152
|
+
else:
|
|
153
|
+
flash(e)
|
|
154
|
+
|
|
155
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
156
|
+
# wait to get a workflow ID
|
|
157
|
+
while not global_config.runner_status:
|
|
158
|
+
time.sleep(1)
|
|
159
|
+
return jsonify({"status": "task started", "task_id": global_config.runner_status.get("id")})
|
|
160
|
+
else:
|
|
161
|
+
return render_template('experiment_run.html', script=script.script_dict, filename=filename,
|
|
162
|
+
dot_py=exec_string, line_collection=line_collection,
|
|
163
|
+
return_list=return_list, config_list=config_list, config_file_list=config_file_list,
|
|
164
|
+
config_preview=config_preview, data_list=data_list, config_type_list=config_type_list,
|
|
165
|
+
no_deck_warning=no_deck_warning, dismiss=dismiss, design_buttons=design_buttons,
|
|
166
|
+
history=deck_list, pause_status=runner.pause_status(), optimizer_schema=optimizers_schema)
|
|
167
|
+
|
|
168
|
+
@execute.route("/executions/campaign", methods=["POST"])
|
|
169
|
+
@login_required
|
|
170
|
+
def run_bo():
|
|
171
|
+
"""
|
|
172
|
+
.. :quickref: Workflow Execution; run Bayesian Optimization
|
|
173
|
+
|
|
174
|
+
Run Bayesian Optimization with the given parameters and objectives.
|
|
175
|
+
|
|
176
|
+
.. http:post:: /executions/campaign
|
|
177
|
+
|
|
178
|
+
:form repeat: number of iterations to run
|
|
179
|
+
:form optimizer_type: type of optimizer to use
|
|
180
|
+
:form existing_data: existing data to use for optimization
|
|
181
|
+
:form parameters: parameters for optimization
|
|
182
|
+
:form objectives: objectives for optimization
|
|
183
|
+
|
|
184
|
+
TODO: merge to experiment_run or not, add more details about the form fields and their expected values.
|
|
185
|
+
"""
|
|
186
|
+
script = utils.get_script_file()
|
|
187
|
+
run_name = script.name if script.name else "untitled"
|
|
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:
|
|
220
|
+
try:
|
|
221
|
+
datapath = current_app.config["DATA_FOLDER"]
|
|
222
|
+
run_name = script.validate_function_name(run_name)
|
|
223
|
+
Optimizer = global_config.optimizers.get(optimizer_type, None)
|
|
224
|
+
if not Optimizer:
|
|
225
|
+
raise ValueError(f"Optimizer {optimizer_type} is not supported or not found.")
|
|
226
|
+
|
|
227
|
+
runner.run_script(script=script, run_name=run_name, optimizer=None,
|
|
228
|
+
logger=g.logger, socketio=g.socketio, repeat_count=repeat,
|
|
229
|
+
output_path=datapath, compiled=False, history=existing_data,
|
|
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
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
237
|
+
return jsonify({"error": e.__str__()})
|
|
238
|
+
else:
|
|
239
|
+
flash(e.__str__())
|
|
240
|
+
return redirect(url_for("execute.experiment_run"))
|
|
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
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@execute.route("/executions/status", methods=["GET"])
|
|
260
|
+
def runner_status():
|
|
261
|
+
"""
|
|
262
|
+
.. :quickref: Workflow Execution Control; backend runner status
|
|
263
|
+
|
|
264
|
+
get is system is busy and current task
|
|
265
|
+
|
|
266
|
+
.. http:get:: /executions/status
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
"""
|
|
270
|
+
# runner = global_config.runner
|
|
271
|
+
runner_busy = global_config.runner_lock.locked()
|
|
272
|
+
status = {"busy": runner_busy}
|
|
273
|
+
task_status = global_config.runner_status
|
|
274
|
+
current_step = {}
|
|
275
|
+
|
|
276
|
+
if task_status is not None:
|
|
277
|
+
task_type = task_status["type"]
|
|
278
|
+
task_id = task_status["id"]
|
|
279
|
+
if task_type == "task":
|
|
280
|
+
# todo
|
|
281
|
+
step = SingleStep.query.get(task_id)
|
|
282
|
+
current_step = step.as_dict()
|
|
283
|
+
if task_type == "workflow":
|
|
284
|
+
workflow = WorkflowRun.query.get(task_id)
|
|
285
|
+
if workflow is not None:
|
|
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(
|
|
289
|
+
WorkflowStep.start_time.desc()).first()
|
|
290
|
+
if latest_step is not None:
|
|
291
|
+
current_step = latest_step.as_dict()
|
|
292
|
+
status["workflow_status"] = {"workflow_info": workflow.as_dict(), "runner_status": runner.get_status()}
|
|
293
|
+
status["current_task"] = current_step
|
|
294
|
+
return jsonify(status), 200
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@execute.route("/executions/abort/next-iteration", methods=["POST"])
|
|
298
|
+
def api_abort_pending():
|
|
299
|
+
"""
|
|
300
|
+
.. :quickref: Workflow Execution control; abort pending workflow
|
|
301
|
+
|
|
302
|
+
finish the current iteration and stop pending workflow iterations
|
|
303
|
+
|
|
304
|
+
.. http:get:: /executions/abort/next-iteration
|
|
305
|
+
|
|
306
|
+
"""
|
|
307
|
+
abort_pending()
|
|
308
|
+
return jsonify({"status": "ok"}), 200
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@execute.route("/executions/abort/next-task", methods=["POST"])
|
|
312
|
+
def api_abort_current():
|
|
313
|
+
"""
|
|
314
|
+
.. :quickref: Workflow Execution Control; abort all pending tasks starting from the next task
|
|
315
|
+
|
|
316
|
+
finish the current task and stop all pending tasks or iterations
|
|
317
|
+
|
|
318
|
+
.. http:get:: /executions/abort/next-task
|
|
319
|
+
|
|
320
|
+
"""
|
|
321
|
+
abort_current()
|
|
322
|
+
return jsonify({"status": "ok"}), 200
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@execute.route("/executions/pause-resume", methods=["POST"])
|
|
326
|
+
def api_pause():
|
|
327
|
+
"""
|
|
328
|
+
.. :quickref: Workflow Execution Control; pause and resume
|
|
329
|
+
|
|
330
|
+
pause workflow iterations or resume workflow iterations
|
|
331
|
+
|
|
332
|
+
.. http:get:: /executions/pause-resume
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
msg = pause()
|
|
336
|
+
return jsonify({"status": "ok", "pause_status": msg}), 200
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@execute.route("/executions/retry", methods=["POST"])
|
|
340
|
+
def api_retry():
|
|
341
|
+
"""
|
|
342
|
+
.. :quickref: Workflow Execution Control; retry the failed workflow execution step.
|
|
343
|
+
|
|
344
|
+
retry the failed workflow execution step.
|
|
345
|
+
|
|
346
|
+
.. http:get:: /executions/retry
|
|
347
|
+
|
|
348
|
+
"""
|
|
349
|
+
retry()
|
|
350
|
+
return jsonify({"status": "ok, retrying failed step"}), 200
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@execute.route('/files/preview/<string:filename>')
|
|
354
|
+
@login_required
|
|
355
|
+
def data_preview(filename):
|
|
356
|
+
"""
|
|
357
|
+
.. :quickref: Workflow Execution Files; preview a workflow history file (.CSV)
|
|
358
|
+
|
|
359
|
+
Preview the contents of a workflow history file in CSV format.
|
|
360
|
+
|
|
361
|
+
.. http:get:: /files/preview/<str:filename>
|
|
362
|
+
"""
|
|
363
|
+
import csv
|
|
364
|
+
import os
|
|
365
|
+
from flask import abort
|
|
366
|
+
|
|
367
|
+
data_folder = current_app.config['DATA_FOLDER']
|
|
368
|
+
file_path = os.path.join(data_folder, filename)
|
|
369
|
+
if not os.path.exists(file_path):
|
|
370
|
+
abort(404)
|
|
371
|
+
with open(file_path, newline='') as csvfile:
|
|
372
|
+
reader = csv.DictReader(csvfile)
|
|
373
|
+
rows = list(reader)
|
|
374
|
+
# Limit preview to first 10 rows
|
|
375
|
+
return jsonify({"columns": reader.fieldnames, "rows": rows})
|
|
376
|
+
|
|
377
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from flask import Blueprint, send_file, request, flash, redirect, url_for, session, current_app
|
|
5
|
+
from werkzeug.utils import secure_filename
|
|
6
|
+
from ivoryos.utils import utils
|
|
7
|
+
|
|
8
|
+
files = Blueprint('execute_files', __name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@files.route('/files/execution-configs')
|
|
12
|
+
def download_empty_config():
|
|
13
|
+
"""
|
|
14
|
+
.. :quickref: Workflow Files; download an empty workflow config file (.CSV)
|
|
15
|
+
|
|
16
|
+
.. http:get:: /files/execution-configs
|
|
17
|
+
|
|
18
|
+
:form file: workflow design CSV file
|
|
19
|
+
:status 302: load pseudo deck and then redirects to :http:get:`/ivoryos/executions/config`
|
|
20
|
+
"""
|
|
21
|
+
script = utils.get_script_file()
|
|
22
|
+
run_name = script.name if script.name else "untitled"
|
|
23
|
+
|
|
24
|
+
filepath = os.path.join(current_app.config['SCRIPT_FOLDER'], f"{run_name}_config.csv")
|
|
25
|
+
with open(filepath, 'w', newline='') as f:
|
|
26
|
+
writer = csv.writer(f)
|
|
27
|
+
cfg, cfg_types = script.config("script")
|
|
28
|
+
writer.writerow(cfg)
|
|
29
|
+
writer.writerow(list(cfg_types.values()))
|
|
30
|
+
return send_file(os.path.abspath(filepath), as_attachment=True)
|
|
31
|
+
|
|
32
|
+
@files.route('/files/batch-configs', methods=['POST'])
|
|
33
|
+
def upload():
|
|
34
|
+
"""
|
|
35
|
+
.. :quickref: Workflow Files; upload a workflow config file (.CSV)
|
|
36
|
+
|
|
37
|
+
.. http:post:: /files/execution-configs
|
|
38
|
+
|
|
39
|
+
:form file: workflow CSV config file
|
|
40
|
+
:status 302: save csv file and then redirects to :http:get:`/ivoryos/executions/config`
|
|
41
|
+
"""
|
|
42
|
+
if request.method == "POST":
|
|
43
|
+
f = request.files['file']
|
|
44
|
+
if 'file' not in request.files:
|
|
45
|
+
flash('No file part')
|
|
46
|
+
if f.filename.split('.')[-1] == "csv":
|
|
47
|
+
filename = secure_filename(f.filename)
|
|
48
|
+
f.save(os.path.join(current_app.config['CSV_FOLDER'], filename))
|
|
49
|
+
session['config_file'] = filename
|
|
50
|
+
return redirect(url_for("execute.experiment_run"))
|
|
51
|
+
else:
|
|
52
|
+
flash("Config file is in csv format")
|
|
53
|
+
return redirect(url_for("execute.experiment_run"))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@files.route('/files/execution-data', methods=['POST'])
|
|
57
|
+
def upload_history():
|
|
58
|
+
"""
|
|
59
|
+
.. :quickref: Workflow Files; upload a workflow history file (.CSV)
|
|
60
|
+
|
|
61
|
+
.. http:post:: /files/execution-data
|
|
62
|
+
|
|
63
|
+
:form file: workflow history CSV file
|
|
64
|
+
:status 302: save csv file and then redirects to :http:get:`/ivoryos/executions/config`
|
|
65
|
+
"""
|
|
66
|
+
if request.method == "POST":
|
|
67
|
+
f = request.files['historyfile']
|
|
68
|
+
if 'historyfile' not in request.files:
|
|
69
|
+
flash('No file part')
|
|
70
|
+
if f.filename.split('.')[-1] == "csv":
|
|
71
|
+
filename = secure_filename(f.filename)
|
|
72
|
+
f.save(os.path.join(current_app.config['DATA_FOLDER'], filename))
|
|
73
|
+
return redirect(url_for("execute.experiment_run"))
|
|
74
|
+
else:
|
|
75
|
+
flash("Config file is in csv format")
|
|
76
|
+
return redirect(url_for("execute.experiment_run"))
|
|
77
|
+
|
|
78
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{# Error modal component for experiment run #}
|
|
2
|
+
<div class="modal fade" id="error-modal" tabindex="-1" aria-labelledby="errorModalLabel" aria-hidden="true">
|
|
3
|
+
<div class="modal-dialog">
|
|
4
|
+
<div class="modal-content">
|
|
5
|
+
<div class="modal-header">
|
|
6
|
+
<h5 class="modal-title" id="errorModalLabel">Error Detected</h5>
|
|
7
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="modal-body">
|
|
10
|
+
<p id="error-message">An error has occurred.</p>
|
|
11
|
+
<p>Do you want to continue execution or stop?</p>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="modal-footer">
|
|
14
|
+
<button type="button" class="btn btn-primary" id="retry-btn" data-bs-dismiss="modal">Rerun Current Step</button>
|
|
15
|
+
<button type="button" class="btn btn-success" id="continue-btn" data-bs-dismiss="modal">Continue</button>
|
|
16
|
+
<button type="button" class="btn btn-danger" id="stop-btn" data-bs-dismiss="modal">Stop Execution</button>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{# Logging panel component for experiment run #}
|
|
2
|
+
<div class="col-lg-6 col-sm-12 logging-panel">
|
|
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>
|
|
19
|
+
</div>
|
|
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;">
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
</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>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{# Progress panel component for experiment run #}
|
|
2
|
+
<div class="col-lg-6 col-sm-12" id="code-panel" style="{{ '' if pause_status else 'display: none;'}}">
|
|
3
|
+
<p>
|
|
4
|
+
<h5>Progress:</h5>
|
|
5
|
+
{% if "prep" in line_collection.keys() %}
|
|
6
|
+
{% set stype = "prep" %}
|
|
7
|
+
<h6>Preparation:</h6>
|
|
8
|
+
{% for code in line_collection["prep"] %}
|
|
9
|
+
<pre style="margin: 0; padding: 0; line-height: 1;"><code class="python" id="{{ stype }}-{{ loop.index0 }}" >{{code}}</code></pre>
|
|
10
|
+
{% endfor %}
|
|
11
|
+
{% endif %}
|
|
12
|
+
{% if "script" in line_collection.keys() %}
|
|
13
|
+
{% set stype = "script" %}
|
|
14
|
+
<h6>Experiment:</h6>
|
|
15
|
+
{% for code in line_collection["script"] %}
|
|
16
|
+
<pre style="margin: 0; padding: 0; line-height: 1;"><code class="python" id="{{ stype }}-{{ loop.index0 }}" >{{code}}</code></pre>
|
|
17
|
+
{% endfor %}
|
|
18
|
+
{% endif %}
|
|
19
|
+
{% if "cleanup" in line_collection.keys() %}
|
|
20
|
+
{% set stype = "cleanup" %}
|
|
21
|
+
<h6>Cleanup:</h6>
|
|
22
|
+
{% for code in line_collection["cleanup"] %}
|
|
23
|
+
<pre style="margin: 0; padding: 0; line-height: 1;"><code class="python" id="{{ stype }}-{{ loop.index0 }}" >{{code}}</code></pre>
|
|
24
|
+
{% endfor %}
|
|
25
|
+
{% endif %}
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{# Run panel component for experiment run #}
|
|
2
|
+
{% if script['script'] or script['prep'] or script['cleanup'] %}
|
|
3
|
+
<div class="col-lg-6 col-sm-12" id="run-panel" style="{{ 'display: none;' if pause_status else '' }}">
|
|
4
|
+
{% include 'components/run_tabs.html' %}
|
|
5
|
+
</div>
|
|
6
|
+
{% else %}
|
|
7
|
+
<div class="col-lg-6 col-sm-12" id="placeholder-panel">
|
|
8
|
+
</div>
|
|
9
|
+
{% endif %}
|