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
|
@@ -5,7 +5,8 @@ from abc import ABC, abstractmethod
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class OptimizerBase(ABC):
|
|
8
|
-
def __init__(self, experiment_name:str, parameter_space: list, objective_config: dict, optimizer_config: dict
|
|
8
|
+
def __init__(self, experiment_name:str, parameter_space: list, objective_config: dict, optimizer_config: dict,
|
|
9
|
+
parameter_constraints:list=None, datapath:str=None):
|
|
9
10
|
"""
|
|
10
11
|
:param experiment_name: arbitrary name
|
|
11
12
|
:param parameter_space: list of parameter names
|
|
@@ -29,6 +30,7 @@ class OptimizerBase(ABC):
|
|
|
29
30
|
self.parameter_space = parameter_space
|
|
30
31
|
self.objective_config = objective_config
|
|
31
32
|
self.optimizer_config = optimizer_config
|
|
33
|
+
self.datapath = datapath
|
|
32
34
|
|
|
33
35
|
@abstractmethod
|
|
34
36
|
def suggest(self, n=1):
|
|
@@ -46,6 +48,23 @@ class OptimizerBase(ABC):
|
|
|
46
48
|
def append_existing_data(self, existing_data):
|
|
47
49
|
pass
|
|
48
50
|
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def get_plots(self, plot_type):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _create_discrete_search_space(range_with_step=None, value_type ="float"):
|
|
57
|
+
if range_with_step is None:
|
|
58
|
+
range_with_step = []
|
|
59
|
+
import numpy as np
|
|
60
|
+
low, high, step = range_with_step
|
|
61
|
+
values = np.arange(low, high + 1e-9 * step, step).tolist()
|
|
62
|
+
if value_type == "float":
|
|
63
|
+
values = [float(v) for v in values]
|
|
64
|
+
if value_type == "int":
|
|
65
|
+
values = [int(v) for v in values]
|
|
66
|
+
return values
|
|
67
|
+
|
|
49
68
|
@staticmethod
|
|
50
69
|
def get_schema():
|
|
51
70
|
"""
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
### Directory: ivoryos/optimizers/baybe_optimizer.py
|
|
2
2
|
from typing import Dict
|
|
3
3
|
|
|
4
|
+
from pandas import DataFrame
|
|
4
5
|
|
|
5
6
|
from ivoryos.utils.utils import install_and_import
|
|
6
7
|
from ivoryos.optimizer.base_optimizer import OptimizerBase
|
|
7
8
|
|
|
8
9
|
class BaybeOptimizer(OptimizerBase):
|
|
9
|
-
def __init__(self, experiment_name, parameter_space, objective_config, optimizer_config
|
|
10
|
+
def __init__(self, experiment_name, parameter_space, objective_config, optimizer_config,
|
|
11
|
+
parameter_constraints:list=None, datapath=None):
|
|
10
12
|
try:
|
|
11
13
|
from baybe import Campaign
|
|
12
14
|
except ImportError:
|
|
13
15
|
install_and_import("baybe")
|
|
14
16
|
print("Please install Baybe with pip install baybe to before register BaybeOptimizer.")
|
|
15
17
|
|
|
16
|
-
super().__init__(experiment_name, parameter_space, objective_config, optimizer_config)
|
|
18
|
+
super().__init__(experiment_name, parameter_space, objective_config, optimizer_config, parameter_constraints, )
|
|
17
19
|
self._trial_id = 0
|
|
18
20
|
self._trials = {}
|
|
19
21
|
|
|
@@ -25,8 +27,8 @@ class BaybeOptimizer(OptimizerBase):
|
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def suggest(self, n=1):
|
|
28
|
-
self.df = self.experiment.recommend(batch_size=n)
|
|
29
|
-
return self.
|
|
30
|
+
# self.df = self.experiment.recommend(batch_size=n)
|
|
31
|
+
return self.experiment.recommend(batch_size=n).to_dict(orient="records")
|
|
30
32
|
|
|
31
33
|
def observe(self, results, index=None):
|
|
32
34
|
"""
|
|
@@ -35,21 +37,19 @@ class BaybeOptimizer(OptimizerBase):
|
|
|
35
37
|
:param index: The index of the trial in the DataFrame, if applicable.
|
|
36
38
|
|
|
37
39
|
"""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
self.experiment.add_measurements(self.df)
|
|
40
|
+
df = DataFrame(results)
|
|
41
|
+
self.experiment.add_measurements(df)
|
|
41
42
|
|
|
42
|
-
def append_existing_data(self, existing_data:
|
|
43
|
+
def append_existing_data(self, existing_data: DataFrame):
|
|
43
44
|
"""
|
|
44
45
|
Append existing data to the Ax experiment.
|
|
45
46
|
:param existing_data: A dictionary containing existing data.
|
|
46
47
|
"""
|
|
47
|
-
|
|
48
|
-
if not existing_data:
|
|
48
|
+
if existing_data.empty:
|
|
49
49
|
return
|
|
50
50
|
# parameter_names = [i.get("name") for i in self.parameter_space]
|
|
51
51
|
# objective_names = [i.get("name") for i in self.objective_config]
|
|
52
|
-
self.experiment.add_measurements(
|
|
52
|
+
self.experiment.add_measurements(existing_data)
|
|
53
53
|
# for name, value in existing_data.items():
|
|
54
54
|
# # First attach the trial and note the trial index
|
|
55
55
|
# parameters = {name: value for name in existing_data if name in parameter_names}
|
|
@@ -65,9 +65,11 @@ class BaybeOptimizer(OptimizerBase):
|
|
|
65
65
|
:param parameter_space: The parameter space configuration.
|
|
66
66
|
[
|
|
67
67
|
{"name": "param_1", "type": "range", "bounds": [1.0, 2.0], "value_type": "float"},
|
|
68
|
-
{"name": "param_2", "type": "
|
|
69
|
-
|
|
70
|
-
{"name": "
|
|
68
|
+
{"name": "param_2", "type": "range", "bounds": [1.0, 2.0, 0.5], "value_type": "float"},
|
|
69
|
+
|
|
70
|
+
{"name": "param_3", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"},
|
|
71
|
+
{"name": "param_4", "type": "range", "bounds": [0 10], "value_type": "int"},
|
|
72
|
+
{"name": "param_5", "type": "substance", "bounds": ["methanol", "water", "toluene"], "value_type": "str"} #TODO
|
|
71
73
|
]
|
|
72
74
|
:return: A list of Baybe parameters.
|
|
73
75
|
"""
|
|
@@ -77,7 +79,10 @@ class BaybeOptimizer(OptimizerBase):
|
|
|
77
79
|
parameters = []
|
|
78
80
|
for p in parameter_space:
|
|
79
81
|
if p["type"] == "range":
|
|
80
|
-
if p["
|
|
82
|
+
if len(p["bounds"]) == 3:
|
|
83
|
+
values = self._create_discrete_search_space(range_with_step=p["bounds"],value_type=p["value_type"])
|
|
84
|
+
parameters.append(NumericalDiscreteParameter(name=p["name"], values=values))
|
|
85
|
+
elif p["value_type"] == "float":
|
|
81
86
|
parameters.append(NumericalContinuousParameter(name=p["name"], bounds=p["bounds"]))
|
|
82
87
|
elif p["value_type"] == "int":
|
|
83
88
|
values = tuple([int(v) for v in range(p["bounds"][0], p["bounds"][1] + 1)])
|
|
@@ -106,10 +111,10 @@ class BaybeOptimizer(OptimizerBase):
|
|
|
106
111
|
weights = []
|
|
107
112
|
for obj in objective_config:
|
|
108
113
|
obj_name = obj.get("name")
|
|
109
|
-
minimize = obj.get("minimize",
|
|
114
|
+
minimize = obj.get("minimize", True)
|
|
110
115
|
weight = obj.get("weight", 1)
|
|
111
116
|
weights.append(weight)
|
|
112
|
-
targets.append(NumericalTarget(name=obj_name,
|
|
117
|
+
targets.append(NumericalTarget(name=obj_name, minimize=minimize))
|
|
113
118
|
|
|
114
119
|
if len(targets) == 1:
|
|
115
120
|
return SingleTargetObjective(target=targets[0])
|
|
@@ -148,6 +153,9 @@ class BaybeOptimizer(OptimizerBase):
|
|
|
148
153
|
recommender=step_2_recommender
|
|
149
154
|
)
|
|
150
155
|
|
|
156
|
+
def get_plots(self, plot_type):
|
|
157
|
+
return None
|
|
158
|
+
|
|
151
159
|
@staticmethod
|
|
152
160
|
def get_schema():
|
|
153
161
|
"""
|
|
@@ -156,6 +164,8 @@ class BaybeOptimizer(OptimizerBase):
|
|
|
156
164
|
return {
|
|
157
165
|
"parameter_types": ["range", "choice", "substance"],
|
|
158
166
|
"multiple_objectives": True,
|
|
167
|
+
"supports_continuous": True,
|
|
168
|
+
"supports_constraints": False,
|
|
159
169
|
"optimizer_config": {
|
|
160
170
|
"step_1": {"model": ["Random", "FPS"], "num_samples": 10},
|
|
161
171
|
"step_2": {"model": ["BOTorch", "Naive Hybrid Space"]}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
### ivoryos/optimizers/nimo_optimizer.py
|
|
2
|
+
import glob
|
|
3
|
+
import itertools
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from ivoryos.optimizer.base_optimizer import OptimizerBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NIMOOptimizer(OptimizerBase):
|
|
11
|
+
def __init__(self, experiment_name:str, parameter_space: list, objective_config: list, optimizer_config: dict,
|
|
12
|
+
parameter_constraints:list=None, datapath:str=None):
|
|
13
|
+
"""
|
|
14
|
+
:param experiment_name: arbitrary name
|
|
15
|
+
:param parameter_space: list of parameter names
|
|
16
|
+
[
|
|
17
|
+
{"name": "param_1", "type": "range", "bounds": [1.0, 2.0], "value_type": "float"},
|
|
18
|
+
{"name": "param_2", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"},
|
|
19
|
+
{"name": "param_3", "type": "range", "bounds": [0 10], "value_type": "int"},
|
|
20
|
+
]
|
|
21
|
+
:param objective_config: objective configuration
|
|
22
|
+
[
|
|
23
|
+
{"name": "obj_1", "minimize": True, "weight": 1},
|
|
24
|
+
{"name": "obj_2", "minimize": False, "weight": 1}
|
|
25
|
+
]
|
|
26
|
+
:param optimizer_config: optimizer configuration
|
|
27
|
+
{
|
|
28
|
+
"step_1": {"model": "RE", "num_samples": 10},
|
|
29
|
+
"step_2": {"model": "PDC"}
|
|
30
|
+
}
|
|
31
|
+
"""
|
|
32
|
+
self.current_step = 0
|
|
33
|
+
self.experiment_name = experiment_name
|
|
34
|
+
self.parameter_space = parameter_space
|
|
35
|
+
self.objective_config = objective_config
|
|
36
|
+
self.optimizer_config = optimizer_config
|
|
37
|
+
|
|
38
|
+
super().__init__(experiment_name, parameter_space, objective_config, optimizer_config, parameter_constraints, datapath)
|
|
39
|
+
|
|
40
|
+
os.makedirs(os.path.join(self.datapath, "nimo_data"), exist_ok=True)
|
|
41
|
+
|
|
42
|
+
step_1 = optimizer_config.get("step_1", {})
|
|
43
|
+
step_2 = optimizer_config.get("step_2", {})
|
|
44
|
+
self.step_1_generator = step_1.get("model", "RE")
|
|
45
|
+
self.step_1_batch_num = step_1.get("num_samples", 1)
|
|
46
|
+
self.step_2_generator = step_2.get("model", "PDC")
|
|
47
|
+
self.candidates = os.path.join(self.datapath, "nimo_data", f"{self.experiment_name}_candidates.csv")
|
|
48
|
+
self.proposals = os.path.join(self.datapath, "nimo_data", f"{self.experiment_name}_proposals.csv")
|
|
49
|
+
self.n_objectives = len(self.objective_config)
|
|
50
|
+
self._create_candidates_csv()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _create_candidates_csv(self):
|
|
54
|
+
# Extract parameter names and their possible values
|
|
55
|
+
import pandas as pd
|
|
56
|
+
import nimo
|
|
57
|
+
import numpy as np
|
|
58
|
+
if os.path.exists(self.candidates) and nimo.history(self.candidates, self.n_objectives):
|
|
59
|
+
return
|
|
60
|
+
param_names = [p["name"] for p in self.parameter_space]
|
|
61
|
+
|
|
62
|
+
param_values = []
|
|
63
|
+
for p in self.parameter_space:
|
|
64
|
+
if p["type"] == "choice" and isinstance(p["bounds"], list):
|
|
65
|
+
param_values.append(p["bounds"])
|
|
66
|
+
elif p["type"] == "range" and len(p["bounds"]) == 3:
|
|
67
|
+
values = self._create_discrete_search_space(range_with_step=p["bounds"],value_type=p["value_type"])
|
|
68
|
+
param_values.append(values)
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError(f"Unsupported parameter format: {p}")
|
|
71
|
+
|
|
72
|
+
# Generate all possible combinations
|
|
73
|
+
combos = list(itertools.product(*param_values))
|
|
74
|
+
|
|
75
|
+
# Create a DataFrame with parameter columns
|
|
76
|
+
df = pd.DataFrame(combos, columns=param_names)
|
|
77
|
+
# Add empty objective columns
|
|
78
|
+
for obj in self.objective_config:
|
|
79
|
+
df[obj["name"]] = ""
|
|
80
|
+
|
|
81
|
+
# Save to CSV
|
|
82
|
+
df.to_csv(self.candidates, index=False)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def suggest(self, n=1):
|
|
86
|
+
import pandas as pd
|
|
87
|
+
import nimo
|
|
88
|
+
method = self.step_1_generator if self.current_step <= self.step_1_batch_num else self.step_2_generator
|
|
89
|
+
nimo.selection(method = method,
|
|
90
|
+
input_file = self.candidates,
|
|
91
|
+
output_file = self.proposals,
|
|
92
|
+
num_objectives = self.n_objectives,
|
|
93
|
+
num_proposals = n)
|
|
94
|
+
self.current_step += 1
|
|
95
|
+
# Read proposals from CSV file
|
|
96
|
+
proposals_df = pd.read_csv(self.proposals)
|
|
97
|
+
# Get parameter names
|
|
98
|
+
param_names = [p["name"] for p in self.parameter_space]
|
|
99
|
+
# Convert proposals to list of parameter dictionaries
|
|
100
|
+
proposals = []
|
|
101
|
+
for _, row in proposals_df.iterrows():
|
|
102
|
+
proposal = {name: row[name] for name in param_names}
|
|
103
|
+
proposals.append(proposal)
|
|
104
|
+
return proposals
|
|
105
|
+
|
|
106
|
+
def _convert_observation_to_list(self, obs: dict) -> list:
|
|
107
|
+
obj_names = [o["name"] for o in self.objective_config]
|
|
108
|
+
return [obs.get(name, None) for name in obj_names]
|
|
109
|
+
|
|
110
|
+
def observe(self, results: list):
|
|
111
|
+
"""
|
|
112
|
+
observe single output, nimo obj input is [1,2,3] or [[1, 2], [1, 2], [1, 2]] for MO
|
|
113
|
+
:param results: [{"objective_name": "value"}, {"objective_name": "value"}]]
|
|
114
|
+
"""
|
|
115
|
+
import nimo
|
|
116
|
+
nimo_objective_values = [self._convert_observation_to_list(result) for result in results]
|
|
117
|
+
nimo.output_update(input_file=self.proposals,
|
|
118
|
+
output_file=self.candidates,
|
|
119
|
+
num_objectives=self.n_objectives,
|
|
120
|
+
objective_values=nimo_objective_values)
|
|
121
|
+
|
|
122
|
+
def append_existing_data(self, existing_data):
|
|
123
|
+
# TODO, history is part of the candidate file, we probably won't need this
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
def get_plots(self, plot_type):
|
|
127
|
+
import nimo
|
|
128
|
+
nimo.visualization.plot_phase_diagram.plot(input_file=self.candidates,
|
|
129
|
+
fig_folder=os.path.join(self.datapath, "nimo_data"))
|
|
130
|
+
files = sorted(glob.glob(os.path.join(os.path.join(self.datapath, "nimo_data"), "phase_diagram_*.png")))
|
|
131
|
+
if not files:
|
|
132
|
+
return None
|
|
133
|
+
return files[-1]
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def get_schema():
|
|
137
|
+
return {
|
|
138
|
+
"parameter_types": ["choice", "range"],
|
|
139
|
+
"multiple_objectives": True,
|
|
140
|
+
"supports_continuous": False,
|
|
141
|
+
"supports_constraints": False,
|
|
142
|
+
"optimizer_config": {
|
|
143
|
+
"step_1": {"model": ["RE", "ES"], "num_samples": 5},
|
|
144
|
+
"step_2": {"model": ["PHYSBO", "PDC", "BLOX", "PTR", "SLESA", "BOMP", "COMBI"]}
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
parameter_space = [
|
|
153
|
+
{"name": "silica", "type": "choice", "bounds": [100], "value_type": "float"},
|
|
154
|
+
{"name": "water", "type": "range", "bounds": [500, 900, 50], "value_type": "float"},
|
|
155
|
+
{"name": "PVA", "type": "choice", "bounds": [0, 0.005, 0.0075, 0.01, 0.05, 0.075, 0.1], "value_type": "float"},
|
|
156
|
+
{"name": "SDS", "type": "choice", "bounds": [0], "value_type": "float"},
|
|
157
|
+
{"name": "DTAB", "type": "choice", "bounds": [0, 0.005, 0.0075, 0.01, 0.05, 0.075, 0.1], "value_type": "float"},
|
|
158
|
+
{"name": "PVP", "type": "choice", "bounds": [0], "value_type": "float"},
|
|
159
|
+
]
|
|
160
|
+
objective_config = [
|
|
161
|
+
{"name": "objective", "minimize": False, "weight": 1},
|
|
162
|
+
|
|
163
|
+
]
|
|
164
|
+
optimizer_config = {
|
|
165
|
+
"step_1": {"model": "RE", "num_samples": 10},
|
|
166
|
+
"step_2": {"model": "PDC"}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
nimo_optimizer = NIMOOptimizer(experiment_name="example_experiment", optimizer_config=optimizer_config, parameter_space=parameter_space, objective_config=objective_config)
|
|
170
|
+
nimo_optimizer.suggest(n=1)
|
|
171
|
+
nimo_optimizer.observe(
|
|
172
|
+
results=[{"objective": 1.0}]
|
|
173
|
+
)
|
ivoryos/optimizer/registry.py
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from ivoryos.optimizer.ax_optimizer import AxOptimizer
|
|
4
4
|
from ivoryos.optimizer.baybe_optimizer import BaybeOptimizer
|
|
5
|
+
from ivoryos.optimizer.nimo_optimizer import NIMOOptimizer
|
|
5
6
|
|
|
6
7
|
OPTIMIZER_REGISTRY = {
|
|
7
8
|
"ax": AxOptimizer,
|
|
8
|
-
"baybe": BaybeOptimizer
|
|
9
|
+
"baybe": BaybeOptimizer,
|
|
10
|
+
"nimo": NIMOOptimizer,
|
|
9
11
|
}
|
ivoryos/routes/auth/auth.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from flask import Blueprint, redirect, url_for, flash, request, render_template, session
|
|
2
|
-
from flask_login import login_required, login_user, logout_user, LoginManager
|
|
2
|
+
from flask_login import login_required, login_user, logout_user, LoginManager, current_user
|
|
3
3
|
import bcrypt
|
|
4
|
+
from sqlalchemy_utils.types import password
|
|
4
5
|
|
|
5
6
|
from ivoryos.utils.db_models import Script, User, db
|
|
6
7
|
from ivoryos.utils.utils import post_script_file
|
|
@@ -15,16 +16,16 @@ def login():
|
|
|
15
16
|
"""
|
|
16
17
|
.. :quickref: User; login user
|
|
17
18
|
|
|
18
|
-
.. http:get:: /login
|
|
19
|
+
.. http:get:: /auth/login
|
|
19
20
|
|
|
20
21
|
load user login form.
|
|
21
22
|
|
|
22
|
-
.. http:post:: /login
|
|
23
|
+
.. http:post:: /auth/login
|
|
23
24
|
|
|
24
25
|
:form username: username
|
|
25
26
|
:form password: password
|
|
26
27
|
:status 302: and then redirects to homepage
|
|
27
|
-
:status 401: incorrect password, redirects to :http:get:`/ivoryos/login`
|
|
28
|
+
:status 401: incorrect password, redirects to :http:get:`/ivoryos/auth/login`
|
|
28
29
|
"""
|
|
29
30
|
if request.method == 'POST':
|
|
30
31
|
username = request.form.get('username')
|
|
@@ -57,16 +58,16 @@ def signup():
|
|
|
57
58
|
"""
|
|
58
59
|
.. :quickref: User; signup for a new account
|
|
59
60
|
|
|
60
|
-
.. http:get:: /signup
|
|
61
|
+
.. http:get:: /auth/signup
|
|
61
62
|
|
|
62
63
|
load user sighup
|
|
63
64
|
|
|
64
|
-
.. http:post:: /signup
|
|
65
|
+
.. http:post:: /auth/signup
|
|
65
66
|
|
|
66
67
|
:form username: username
|
|
67
68
|
:form password: password
|
|
68
|
-
:status 302: and then redirects to :http:get:`/ivoryos/login`
|
|
69
|
-
:status 409: when user already exists, redirects to :http:get:`/ivoryos/signup`
|
|
69
|
+
:status 302: and then redirects to :http:get:`/ivoryos/auth/login`
|
|
70
|
+
:status 409: when user already exists, redirects to :http:get:`/ivoryos/auth/signup`
|
|
70
71
|
"""
|
|
71
72
|
if request.method == 'POST':
|
|
72
73
|
username = request.form.get('username')
|
|
@@ -85,6 +86,30 @@ def signup():
|
|
|
85
86
|
return redirect(url_for('auth.login'))
|
|
86
87
|
return render_template('signup.html')
|
|
87
88
|
|
|
89
|
+
@auth.route("/change-password", methods=['GET', 'POST'])
|
|
90
|
+
@login_required
|
|
91
|
+
def change_password():
|
|
92
|
+
"""
|
|
93
|
+
.. :quickref: User; change password
|
|
94
|
+
|
|
95
|
+
.. http:get:: /auth/change-password
|
|
96
|
+
|
|
97
|
+
.. http:post:: /auth/change-password
|
|
98
|
+
|
|
99
|
+
change password
|
|
100
|
+
"""
|
|
101
|
+
if request.method == "POST":
|
|
102
|
+
old_password = request.form.get("old_password")
|
|
103
|
+
new_password = request.form.get("new_password")
|
|
104
|
+
# confirm_password = request.form.get("confirm_password")
|
|
105
|
+
user = User.query.filter_by(username=current_user.get_id()).first()
|
|
106
|
+
if not bcrypt.checkpw(old_password.encode('utf-8'), user.hashPassword):
|
|
107
|
+
flash("Incorrect password")
|
|
108
|
+
return redirect(url_for("auth.change_password"))
|
|
109
|
+
user.hashPassword = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
|
|
110
|
+
db.session.commit()
|
|
111
|
+
return redirect(url_for("main.index"))
|
|
112
|
+
return render_template("change_password.html")
|
|
88
113
|
|
|
89
114
|
@auth.route("/logout")
|
|
90
115
|
@login_required
|
|
@@ -92,6 +117,8 @@ def logout():
|
|
|
92
117
|
"""
|
|
93
118
|
.. :quickref: User; logout the user
|
|
94
119
|
|
|
120
|
+
.. http:get:: /auth/logout
|
|
121
|
+
|
|
95
122
|
logout the current user, clear session info, and redirect to the login page.
|
|
96
123
|
"""
|
|
97
124
|
logout_user()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{% extends 'base.html' %}
|
|
2
|
+
{% block title %}IvoryOS | Change Password{% endblock %}
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
{% block body %}
|
|
6
|
+
<div class= "login">
|
|
7
|
+
<div class="bg-white rounded shadow-sm flex-fill">
|
|
8
|
+
<div class="p-4" style="align: center">
|
|
9
|
+
<h5>Change Password</h5>
|
|
10
|
+
<form role="form" method='POST' name="signup" action="{{ url_for('auth.change_password') }}">
|
|
11
|
+
|
|
12
|
+
<div class="input-group mb-3">
|
|
13
|
+
<label class="input-group-text" for="old_password">Old Password</label>
|
|
14
|
+
<input class="form-control" type="password" id="old_password" name="old_password" required>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="input-group mb-3">
|
|
17
|
+
<label class="input-group-text" for="new_password">New Password</label>
|
|
18
|
+
<input class="form-control" type="password" id="new_password" name="new_password" required>
|
|
19
|
+
</div>
|
|
20
|
+
{# <div class="input-group mb-3">#}
|
|
21
|
+
{# <label class="input-group-text" for="confirm_password">Confirm Password</label>#}
|
|
22
|
+
{# <input class="form-control" type="confirm_password" id="confirm_password" name="confirm_password">#}
|
|
23
|
+
{# </div>#}
|
|
24
|
+
|
|
25
|
+
<button type="submit" class="btn btn-secondary" name="login" style="width: 100%;">Confirm</button>
|
|
26
|
+
</form>
|
|
27
|
+
<p class="message" >Need another account? <a href="{{ url_for('auth.signup') }}">Sign up</a></p>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{% endblock %}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
|
|
1
3
|
from flask import Blueprint, redirect, flash, request, render_template, session, current_app, jsonify
|
|
2
4
|
from flask_login import login_required
|
|
3
5
|
|
|
@@ -5,7 +7,7 @@ from ivoryos.routes.control.control_file import control_file
|
|
|
5
7
|
from ivoryos.routes.control.control_new_device import control_temp
|
|
6
8
|
from ivoryos.routes.control.utils import post_session_by_instrument, get_session_by_instrument, find_instrument_by_name
|
|
7
9
|
from ivoryos.utils.global_config import GlobalConfig
|
|
8
|
-
from ivoryos.utils.form import create_form_from_module
|
|
10
|
+
from ivoryos.utils.form import create_form_from_module, create_form_from_pseudo
|
|
9
11
|
from ivoryos.utils.task_runner import TaskRunner
|
|
10
12
|
|
|
11
13
|
global_config = GlobalConfig()
|
|
@@ -21,7 +23,7 @@ control.register_blueprint(control_temp)
|
|
|
21
23
|
@control.route("/", strict_slashes=False, methods=["GET", "POST"])
|
|
22
24
|
@control.route("/<string:instrument>", strict_slashes=False, methods=["GET", "POST"])
|
|
23
25
|
@login_required
|
|
24
|
-
def deck_controllers():
|
|
26
|
+
async def deck_controllers(instrument: str = None):
|
|
25
27
|
"""
|
|
26
28
|
.. :quickref: Direct Control; device (instruments) and methods
|
|
27
29
|
|
|
@@ -44,43 +46,71 @@ def deck_controllers():
|
|
|
44
46
|
:status 200: render template with instruments and methods
|
|
45
47
|
|
|
46
48
|
"""
|
|
47
|
-
|
|
48
|
-
temp_variables = global_config.defined_variables.keys()
|
|
49
|
-
instrument = request.args.get('instrument')
|
|
49
|
+
instrument = instrument or request.args.get("instrument")
|
|
50
50
|
forms = None
|
|
51
51
|
if instrument:
|
|
52
52
|
inst_object = find_instrument_by_name(instrument)
|
|
53
|
-
|
|
53
|
+
if instrument.startswith("blocks"):
|
|
54
|
+
forms = create_form_from_pseudo(pseudo=inst_object, autofill=False, design=False)
|
|
55
|
+
else:
|
|
56
|
+
forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
|
|
54
57
|
order = get_session_by_instrument('card_order', instrument)
|
|
55
58
|
hidden_functions = get_session_by_instrument('hidden_functions', instrument)
|
|
56
|
-
functions = list(
|
|
59
|
+
functions = list(forms.keys())
|
|
57
60
|
for function in functions:
|
|
58
61
|
if function not in hidden_functions and function not in order:
|
|
59
62
|
order.append(function)
|
|
60
63
|
post_session_by_instrument('card_order', instrument, order)
|
|
61
|
-
forms = {name:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
64
|
+
forms = {name: forms[name] for name in order if name in forms}
|
|
65
|
+
|
|
66
|
+
if request.method == "POST":
|
|
67
|
+
if not forms:
|
|
68
|
+
return jsonify({"success": False, "error": "Instrument not found"}), 404
|
|
69
|
+
|
|
70
|
+
payload = request.get_json() if request.is_json else request.form.to_dict()
|
|
71
|
+
method_name = payload.pop("hidden_name", None)
|
|
72
|
+
form = forms.get(method_name)
|
|
73
|
+
|
|
74
|
+
if not form:
|
|
75
|
+
return jsonify({"success": False, "error": f"Method {method_name} not found"}), 404
|
|
76
|
+
|
|
77
|
+
# Extract kwargs
|
|
78
|
+
if request.is_json:
|
|
79
|
+
kwargs = {k: v for k, v in payload.items() if k not in ["csrf_token", "hidden_wait"]}
|
|
80
|
+
else:
|
|
81
|
+
kwargs = {field.name: field.data for field in form if field.name not in ["csrf_token", "hidden_name"]}
|
|
82
|
+
|
|
83
|
+
wait = str(payload.get("hidden_wait", "true")).lower() == "true"
|
|
84
|
+
|
|
85
|
+
output = await runner.run_single_step(
|
|
86
|
+
component=instrument, method=method_name, kwargs=kwargs, wait=wait,
|
|
87
|
+
current_app=current_app._get_current_object()
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if request.is_json:
|
|
91
|
+
return jsonify(output)
|
|
92
|
+
else:
|
|
93
|
+
if output.get("success"):
|
|
94
|
+
flash(f"Run Success! Output: {output.get('output', 'None')}")
|
|
75
95
|
else:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
96
|
+
flash(f"Run Error! {output.get('output', 'Unknown error occurred.')}", "error")
|
|
97
|
+
|
|
98
|
+
# GET request → render web form or return snapshot for API
|
|
99
|
+
if request.is_json or request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
100
|
+
# 1.3.2 fix snapshot copy, add building blocks to snapshots
|
|
101
|
+
snapshot = copy.deepcopy(global_config.deck_snapshot)
|
|
102
|
+
building_blocks = copy.deepcopy(global_config.building_blocks)
|
|
103
|
+
snapshot.update(building_blocks)
|
|
104
|
+
for instrument_key, instrument_data in snapshot.items():
|
|
105
|
+
for function_key, function_data in instrument_data.items():
|
|
106
|
+
function_data["signature"] = str(function_data["signature"])
|
|
107
|
+
return jsonify(snapshot)
|
|
108
|
+
|
|
80
109
|
return render_template(
|
|
81
|
-
|
|
82
|
-
defined_variables=
|
|
83
|
-
|
|
110
|
+
"controllers.html",
|
|
111
|
+
defined_variables=global_config.deck_snapshot.keys(),
|
|
112
|
+
block_variables=global_config.building_blocks.keys(),
|
|
113
|
+
temp_variables=global_config.defined_variables.keys(),
|
|
84
114
|
instrument=instrument,
|
|
85
115
|
forms=forms,
|
|
86
116
|
session=session
|
|
@@ -2,7 +2,7 @@ import os
|
|
|
2
2
|
from flask import Blueprint, request,current_app, send_file
|
|
3
3
|
from flask_login import login_required
|
|
4
4
|
|
|
5
|
-
from ivoryos.utils.client_proxy import
|
|
5
|
+
from ivoryos.utils.client_proxy import ProxyGenerator
|
|
6
6
|
from ivoryos.utils.global_config import GlobalConfig
|
|
7
7
|
|
|
8
8
|
global_config = GlobalConfig()
|
|
@@ -10,27 +10,24 @@ global_config = GlobalConfig()
|
|
|
10
10
|
control_file = Blueprint('file', __name__)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
|
|
13
14
|
@control_file.route("/files/proxy", strict_slashes=False)
|
|
14
15
|
@login_required
|
|
15
16
|
def download_proxy():
|
|
16
17
|
"""
|
|
17
|
-
.. :quickref: Direct Control Files;
|
|
18
|
+
.. :quickref: Direct Control Files; Download proxy Python interface
|
|
18
19
|
|
|
19
20
|
download proxy Python interface
|
|
20
21
|
|
|
21
22
|
.. http:get:: /files/proxy
|
|
22
23
|
"""
|
|
24
|
+
generator = ProxyGenerator(request.url_root)
|
|
23
25
|
snapshot = global_config.deck_snapshot.copy()
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class_definitions[class_name.capitalize()] = create_function(request.url_root, class_name, instrument_data)
|
|
33
|
-
# Export the generated class definitions to a .py script
|
|
34
|
-
export_to_python(class_definitions, current_app.config["OUTPUT_FOLDER"])
|
|
35
|
-
filepath = os.path.join(current_app.config["OUTPUT_FOLDER"], "generated_proxy.py")
|
|
36
|
-
return send_file(os.path.abspath(filepath), as_attachment=True)
|
|
26
|
+
|
|
27
|
+
filepath = generator.generate_from_flask_route(
|
|
28
|
+
snapshot,
|
|
29
|
+
request.url_root,
|
|
30
|
+
current_app.config["OUTPUT_FOLDER"]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return send_file(os.path.abspath(filepath), as_attachment=True)
|