ivoryos 1.3.9__py3-none-any.whl → 1.4.1__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.
Potentially problematic release.
This version of ivoryos might be problematic. Click here for more details.
- ivoryos/optimizer/ax_optimizer.py +47 -25
- ivoryos/optimizer/base_optimizer.py +19 -1
- ivoryos/optimizer/baybe_optimizer.py +27 -17
- ivoryos/optimizer/nimo_optimizer.py +25 -16
- ivoryos/routes/data/data.py +27 -9
- ivoryos/routes/data/templates/components/step_card.html +47 -11
- ivoryos/routes/data/templates/workflow_view.html +14 -5
- ivoryos/routes/design/design.py +31 -1
- ivoryos/routes/design/design_step.py +2 -1
- ivoryos/routes/design/templates/components/edit_action_form.html +16 -3
- ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
- ivoryos/routes/design/templates/experiment_builder.html +1 -0
- ivoryos/routes/execute/execute.py +71 -13
- 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/socket_handlers.py +1 -1
- ivoryos/static/js/action_handlers.js +252 -118
- ivoryos/static/js/sortable_design.js +1 -0
- ivoryos/utils/bo_campaign.py +17 -16
- ivoryos/utils/db_models.py +122 -18
- ivoryos/utils/decorators.py +1 -0
- ivoryos/utils/form.py +32 -12
- ivoryos/utils/nest_script.py +314 -0
- ivoryos/utils/script_runner.py +436 -143
- ivoryos/utils/utils.py +11 -1
- ivoryos/version.py +1 -1
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/METADATA +6 -4
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/RECORD +35 -34
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/WHEEL +0 -0
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/licenses/LICENSE +0 -0
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/top_level.txt +0 -0
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
# optimizers/ax_optimizer.py
|
|
2
2
|
from typing import Dict
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
from pandas import DataFrame
|
|
5
5
|
|
|
6
6
|
from ivoryos.optimizer.base_optimizer import OptimizerBase
|
|
7
7
|
from ivoryos.utils.utils import install_and_import
|
|
8
8
|
|
|
9
9
|
class AxOptimizer(OptimizerBase):
|
|
10
|
-
def __init__(self, experiment_name, parameter_space, objective_config, optimizer_config=None
|
|
10
|
+
def __init__(self, experiment_name, parameter_space, objective_config, optimizer_config=None,
|
|
11
|
+
parameter_constraints:list=None, datapath=None):
|
|
12
|
+
self.trial_index_list = None
|
|
11
13
|
try:
|
|
12
14
|
from ax.api.client import Client
|
|
13
15
|
except ImportError as e:
|
|
14
16
|
install_and_import("ax", "ax-platform")
|
|
15
17
|
raise ImportError("Please install Ax with pip install ax-platform to use AxOptimizer. Attempting to install Ax...")
|
|
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
|
|
|
18
20
|
self.client = Client()
|
|
19
21
|
# 2. Configure where Ax will search.
|
|
20
22
|
self.client.configure_experiment(
|
|
21
23
|
name=experiment_name,
|
|
22
|
-
parameters=self._convert_parameter_to_ax_format(parameter_space)
|
|
24
|
+
parameters=self._convert_parameter_to_ax_format(parameter_space),
|
|
25
|
+
parameter_constraints=parameter_constraints
|
|
23
26
|
)
|
|
24
27
|
# 3. Configure the objective function.
|
|
25
28
|
self.client.configure_optimization(objective=self._convert_objective_to_ax_format(objective_config))
|
|
@@ -48,12 +51,17 @@ class AxOptimizer(OptimizerBase):
|
|
|
48
51
|
ax_params = []
|
|
49
52
|
for p in parameter_space:
|
|
50
53
|
if p["type"] == "range":
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
# if step is used here, convert to ChoiceParameterConfig
|
|
55
|
+
if len(p["bounds"]) == 3:
|
|
56
|
+
values = self._create_discrete_search_space(range_with_step=p["bounds"],value_type=p["value_type"])
|
|
57
|
+
ax_params.append(ChoiceParameterConfig(name=p["name"], values=values, parameter_type="int", is_ordered=True))
|
|
58
|
+
else:
|
|
59
|
+
ax_params.append(
|
|
60
|
+
RangeParameterConfig(
|
|
61
|
+
name=p["name"],
|
|
62
|
+
bounds=tuple(p["bounds"]),
|
|
63
|
+
parameter_type=p["value_type"]
|
|
64
|
+
))
|
|
57
65
|
elif p["type"] == "choice":
|
|
58
66
|
ax_params.append(
|
|
59
67
|
ChoiceParameterConfig(
|
|
@@ -101,15 +109,25 @@ class AxOptimizer(OptimizerBase):
|
|
|
101
109
|
return GenerationStrategy(steps=[generator_1, generator_2])
|
|
102
110
|
|
|
103
111
|
def suggest(self, n=1):
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
trials = self.client.get_next_trials(n)
|
|
113
|
+
trial_index_list = []
|
|
114
|
+
param_list = []
|
|
115
|
+
for trial_index, params in trials.items():
|
|
116
|
+
trial_index_list.append(trial_index)
|
|
117
|
+
param_list.append(params)
|
|
118
|
+
self.trial_index_list = trial_index_list
|
|
119
|
+
return param_list
|
|
107
120
|
|
|
108
121
|
def observe(self, results):
|
|
109
|
-
self.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
for trial_index, result in zip(self.trial_index_list, results):
|
|
123
|
+
obj_only_result = {k: v for k, v in result.items() if k in [obj["name"] for obj in self.objective_config]}
|
|
124
|
+
self.client.complete_trial(
|
|
125
|
+
trial_index=trial_index,
|
|
126
|
+
raw_data=obj_only_result
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def get_plots(self, plot_type):
|
|
130
|
+
return None
|
|
113
131
|
|
|
114
132
|
@staticmethod
|
|
115
133
|
def get_schema():
|
|
@@ -117,29 +135,33 @@ class AxOptimizer(OptimizerBase):
|
|
|
117
135
|
"parameter_types": ["range", "choice"],
|
|
118
136
|
"multiple_objectives": True,
|
|
119
137
|
# "objective_weights": True,
|
|
138
|
+
"supports_continuous": True,
|
|
139
|
+
"supports_constraints": True,
|
|
120
140
|
"optimizer_config": {
|
|
121
141
|
"step_1": {"model": ["Sobol", "Uniform", "Factorial", "Thompson"], "num_samples": 5},
|
|
122
142
|
"step_2": {"model": ["BoTorch", "SAASBO", "SAAS_MTGP", "Legacy_GPEI", "EB", "EB_Ashr", "ST_MTGP", "BO_MIXED", "Contextual_SACBO"]}
|
|
123
143
|
},
|
|
144
|
+
|
|
124
145
|
}
|
|
125
146
|
|
|
126
|
-
def append_existing_data(self, existing_data):
|
|
147
|
+
def append_existing_data(self, existing_data:DataFrame):
|
|
127
148
|
"""
|
|
128
149
|
Append existing data to the Ax experiment.
|
|
129
150
|
:param existing_data: A dictionary containing existing data.
|
|
130
151
|
"""
|
|
131
|
-
|
|
132
|
-
if not existing_data:
|
|
133
|
-
return
|
|
152
|
+
|
|
134
153
|
if isinstance(existing_data, DataFrame):
|
|
154
|
+
if existing_data.empty:
|
|
155
|
+
return
|
|
135
156
|
existing_data = existing_data.to_dict(orient="records")
|
|
136
157
|
parameter_names = [i.get("name") for i in self.parameter_space]
|
|
137
158
|
objective_names = [i.get("name") for i in self.objective_config]
|
|
138
|
-
for
|
|
139
|
-
#
|
|
140
|
-
|
|
159
|
+
for entry in existing_data:
|
|
160
|
+
# for name, value in entry.items():
|
|
161
|
+
# First attach the trial and note the trial index
|
|
162
|
+
parameters = {name: value for name, value in entry.items() if name in parameter_names}
|
|
141
163
|
trial_index = self.client.attach_trial(parameters=parameters)
|
|
142
|
-
raw_data = {name: value for name in
|
|
164
|
+
raw_data = {name: value for name, value in entry.items() if name in objective_names}
|
|
143
165
|
# Then complete the trial with the existing data
|
|
144
166
|
self.client.complete_trial(trial_index=trial_index, raw_data=raw_data)
|
|
145
167
|
|
|
@@ -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
|
|
@@ -47,6 +48,23 @@ class OptimizerBase(ABC):
|
|
|
47
48
|
def append_existing_data(self, existing_data):
|
|
48
49
|
pass
|
|
49
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
|
+
|
|
50
68
|
@staticmethod
|
|
51
69
|
def get_schema():
|
|
52
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"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
### ivoryos/optimizers/nimo_optimizer.py
|
|
2
|
+
import glob
|
|
2
3
|
import itertools
|
|
3
4
|
import os
|
|
4
5
|
|
|
@@ -7,7 +8,8 @@ from ivoryos.optimizer.base_optimizer import OptimizerBase
|
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class NIMOOptimizer(OptimizerBase):
|
|
10
|
-
def __init__(self, experiment_name:str, parameter_space: list, objective_config: list, optimizer_config: dict,
|
|
11
|
+
def __init__(self, experiment_name:str, parameter_space: list, objective_config: list, optimizer_config: dict,
|
|
12
|
+
parameter_constraints:list=None, datapath:str=None):
|
|
11
13
|
"""
|
|
12
14
|
:param experiment_name: arbitrary name
|
|
13
15
|
:param parameter_space: list of parameter names
|
|
@@ -33,7 +35,7 @@ class NIMOOptimizer(OptimizerBase):
|
|
|
33
35
|
self.objective_config = objective_config
|
|
34
36
|
self.optimizer_config = optimizer_config
|
|
35
37
|
|
|
36
|
-
super().__init__(experiment_name, parameter_space, objective_config, optimizer_config, datapath)
|
|
38
|
+
super().__init__(experiment_name, parameter_space, objective_config, optimizer_config, parameter_constraints, datapath)
|
|
37
39
|
|
|
38
40
|
os.makedirs(os.path.join(self.datapath, "nimo_data"), exist_ok=True)
|
|
39
41
|
|
|
@@ -52,6 +54,7 @@ class NIMOOptimizer(OptimizerBase):
|
|
|
52
54
|
# Extract parameter names and their possible values
|
|
53
55
|
import pandas as pd
|
|
54
56
|
import nimo
|
|
57
|
+
import numpy as np
|
|
55
58
|
if os.path.exists(self.candidates) and nimo.history(self.candidates, self.n_objectives):
|
|
56
59
|
return
|
|
57
60
|
param_names = [p["name"] for p in self.parameter_space]
|
|
@@ -60,11 +63,9 @@ class NIMOOptimizer(OptimizerBase):
|
|
|
60
63
|
for p in self.parameter_space:
|
|
61
64
|
if p["type"] == "choice" and isinstance(p["bounds"], list):
|
|
62
65
|
param_values.append(p["bounds"])
|
|
63
|
-
elif p["type"] == "range" and len(p["bounds"]) ==
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
step = (high - low) / (num_points - 1)
|
|
67
|
-
param_values.append([round(low + i * step, 4) for i in range(num_points)])
|
|
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)
|
|
68
69
|
else:
|
|
69
70
|
raise ValueError(f"Unsupported parameter format: {p}")
|
|
70
71
|
|
|
@@ -73,7 +74,6 @@ class NIMOOptimizer(OptimizerBase):
|
|
|
73
74
|
|
|
74
75
|
# Create a DataFrame with parameter columns
|
|
75
76
|
df = pd.DataFrame(combos, columns=param_names)
|
|
76
|
-
|
|
77
77
|
# Add empty objective columns
|
|
78
78
|
for obj in self.objective_config:
|
|
79
79
|
df[obj["name"]] = ""
|
|
@@ -101,20 +101,19 @@ class NIMOOptimizer(OptimizerBase):
|
|
|
101
101
|
for _, row in proposals_df.iterrows():
|
|
102
102
|
proposal = {name: row[name] for name in param_names}
|
|
103
103
|
proposals.append(proposal)
|
|
104
|
-
return proposals
|
|
104
|
+
return proposals
|
|
105
105
|
|
|
106
106
|
def _convert_observation_to_list(self, obs: dict) -> list:
|
|
107
107
|
obj_names = [o["name"] for o in self.objective_config]
|
|
108
108
|
return [obs.get(name, None) for name in obj_names]
|
|
109
109
|
|
|
110
|
-
def observe(self, results:
|
|
110
|
+
def observe(self, results: list):
|
|
111
111
|
"""
|
|
112
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"}
|
|
113
|
+
:param results: [{"objective_name": "value"}, {"objective_name": "value"}]]
|
|
114
114
|
"""
|
|
115
115
|
import nimo
|
|
116
|
-
nimo_objective_values = [self._convert_observation_to_list(results
|
|
117
|
-
|
|
116
|
+
nimo_objective_values = [self._convert_observation_to_list(result) for result in results]
|
|
118
117
|
nimo.output_update(input_file=self.proposals,
|
|
119
118
|
output_file=self.candidates,
|
|
120
119
|
num_objectives=self.n_objectives,
|
|
@@ -124,12 +123,22 @@ class NIMOOptimizer(OptimizerBase):
|
|
|
124
123
|
# TODO, history is part of the candidate file, we probably won't need this
|
|
125
124
|
pass
|
|
126
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]
|
|
127
134
|
|
|
128
135
|
@staticmethod
|
|
129
136
|
def get_schema():
|
|
130
137
|
return {
|
|
131
|
-
"parameter_types": ["choice"],
|
|
138
|
+
"parameter_types": ["choice", "range"],
|
|
132
139
|
"multiple_objectives": True,
|
|
140
|
+
"supports_continuous": False,
|
|
141
|
+
"supports_constraints": False,
|
|
133
142
|
"optimizer_config": {
|
|
134
143
|
"step_1": {"model": ["RE", "ES"], "num_samples": 5},
|
|
135
144
|
"step_2": {"model": ["PHYSBO", "PDC", "BLOX", "PTR", "SLESA", "BOMP", "COMBI"]}
|
|
@@ -142,7 +151,7 @@ class NIMOOptimizer(OptimizerBase):
|
|
|
142
151
|
if __name__ == "__main__":
|
|
143
152
|
parameter_space = [
|
|
144
153
|
{"name": "silica", "type": "choice", "bounds": [100], "value_type": "float"},
|
|
145
|
-
{"name": "water", "type": "
|
|
154
|
+
{"name": "water", "type": "range", "bounds": [500, 900, 50], "value_type": "float"},
|
|
146
155
|
{"name": "PVA", "type": "choice", "bounds": [0, 0.005, 0.0075, 0.01, 0.05, 0.075, 0.1], "value_type": "float"},
|
|
147
156
|
{"name": "SDS", "type": "choice", "bounds": [0], "value_type": "float"},
|
|
148
157
|
{"name": "DTAB", "type": "choice", "bounds": [0, 0.005, 0.0075, 0.01, 0.05, 0.075, 0.1], "value_type": "float"},
|
|
@@ -160,5 +169,5 @@ if __name__ == "__main__":
|
|
|
160
169
|
nimo_optimizer = NIMOOptimizer(experiment_name="example_experiment", optimizer_config=optimizer_config, parameter_space=parameter_space, objective_config=objective_config)
|
|
161
170
|
nimo_optimizer.suggest(n=1)
|
|
162
171
|
nimo_optimizer.observe(
|
|
163
|
-
results={"objective": 1.0}
|
|
172
|
+
results=[{"objective": 1.0}]
|
|
164
173
|
)
|
ivoryos/routes/data/data.py
CHANGED
|
@@ -118,25 +118,43 @@ def workflow_phase_data(workflow_id: int):
|
|
|
118
118
|
|
|
119
119
|
:param workflow_id: workflow id
|
|
120
120
|
"""
|
|
121
|
+
|
|
121
122
|
workflow = db.session.get(WorkflowRun, workflow_id)
|
|
122
123
|
if not workflow:
|
|
123
124
|
return jsonify({})
|
|
124
125
|
|
|
125
126
|
phase_data = {}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
WorkflowPhase.repeat_index).all()
|
|
127
|
+
main_phases = WorkflowPhase.query.filter_by(run_id=workflow_id, name='main') \
|
|
128
|
+
.order_by(WorkflowPhase.repeat_index).all()
|
|
129
129
|
|
|
130
130
|
for phase in main_phases:
|
|
131
131
|
outputs = phase.outputs or {}
|
|
132
132
|
phase_index = phase.repeat_index
|
|
133
|
-
# Convert each key to list of dicts for x (phase_index) and y (value)
|
|
134
133
|
phase_data[phase_index] = {}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
|
|
135
|
+
# Normalize everything to a list of dicts
|
|
136
|
+
if isinstance(outputs, dict):
|
|
137
|
+
outputs = [outputs]
|
|
138
|
+
elif isinstance(outputs, list):
|
|
139
|
+
# flatten if it’s nested like [[{...}, {...}]]
|
|
140
|
+
outputs = [
|
|
141
|
+
item for sublist in outputs
|
|
142
|
+
for item in (sublist if isinstance(sublist, list) else [sublist])
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
# convert each output entry to plotting format
|
|
146
|
+
for out in outputs:
|
|
147
|
+
if not isinstance(out, dict):
|
|
148
|
+
continue
|
|
149
|
+
for k, v in out.items():
|
|
150
|
+
if isinstance(v, (int, float)):
|
|
151
|
+
phase_data[phase_index].setdefault(k, []).append(
|
|
152
|
+
{"x": phase_index, "y": v}
|
|
153
|
+
)
|
|
154
|
+
elif isinstance(v, list) and all(isinstance(i, (int, float)) for i in v):
|
|
155
|
+
phase_data[phase_index].setdefault(k, []).extend(
|
|
156
|
+
{"x": phase_index, "y": val} for val in v
|
|
157
|
+
)
|
|
140
158
|
|
|
141
159
|
return jsonify(phase_data)
|
|
142
160
|
|
|
@@ -4,14 +4,31 @@
|
|
|
4
4
|
<i class="fas fa-play-circle me-1"></i> Start: {{ phase.start_time.strftime('%H:%M:%S') if phase.start_time else 'N/A' }}
|
|
5
5
|
<i class="fas fa-stop-circle ms-2 me-1"></i> End: {{ phase.end_time.strftime('%H:%M:%S') if phase.end_time else 'N/A' }}
|
|
6
6
|
</small>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
{% if phase.parameters %}
|
|
8
|
+
<div class="mt-2">
|
|
9
|
+
<strong>Parameters: </strong>
|
|
10
|
+
{% if phase.parameters is mapping %}
|
|
11
|
+
{% if phase.parameters %}
|
|
12
|
+
<div class="mt-2">
|
|
13
|
+
<strong>Parameters: </strong>
|
|
14
|
+
{% for key, value in phase.parameters.items() %}
|
|
15
|
+
<span class="badge bg-secondary me-1">{{ key }}: {{ value }}</span>
|
|
16
|
+
{% endfor %}
|
|
17
|
+
</div>
|
|
18
|
+
{% endif %}
|
|
19
|
+
{% else %}
|
|
20
|
+
{% for batch in phase.parameters %}
|
|
21
|
+
<div class="mt-1">
|
|
22
|
+
<span class="badge bg-info text-dark me-1">Batch {{ loop.index }}</span>
|
|
23
|
+
{% for key, value in batch.items() %}
|
|
24
|
+
<span class="badge bg-secondary me-1">{{ key }}: {{ value }}</span>
|
|
25
|
+
{% endfor %}
|
|
26
|
+
</div>
|
|
27
|
+
{% endfor %}
|
|
14
28
|
{% endif %}
|
|
29
|
+
</div>
|
|
30
|
+
{% endif %}
|
|
31
|
+
|
|
15
32
|
{% if phase.steps %}
|
|
16
33
|
<div class="mt-2">
|
|
17
34
|
<strong>Steps:</strong>
|
|
@@ -29,13 +46,32 @@
|
|
|
29
46
|
</div>
|
|
30
47
|
{% endif %}
|
|
31
48
|
{% if phase.outputs %}
|
|
32
|
-
<div class="mt-
|
|
49
|
+
<div class="mt-2">
|
|
33
50
|
<strong>Outputs:</strong>
|
|
34
|
-
|
|
51
|
+
|
|
52
|
+
{% if phase.outputs is mapping %}
|
|
35
53
|
{% for key, value in phase.outputs.items() %}
|
|
36
|
-
<
|
|
54
|
+
<span class="badge bg-success me-1">{{ key }}: {{ value }}</span>
|
|
37
55
|
{% endfor %}
|
|
38
|
-
|
|
56
|
+
|
|
57
|
+
{% elif phase.outputs is sequence %}
|
|
58
|
+
{% for batch in phase.outputs %}
|
|
59
|
+
<div class="mt-1">
|
|
60
|
+
<span class="badge bg-info text-dark me-1">Batch {{ loop.index }}</span>
|
|
61
|
+
{% if batch is mapping %}
|
|
62
|
+
{% for key, value in batch.items() %}
|
|
63
|
+
<span class="badge bg-success me-1">{{ key }}: {{ value }}</span>
|
|
64
|
+
{% endfor %}
|
|
65
|
+
{% elif batch is sequence %}
|
|
66
|
+
{% for kwargs in batch %}
|
|
67
|
+
{% for key, value in kwargs.items() %}
|
|
68
|
+
<span class="badge bg-success me-1">{{ key }}: {{ value }}</span>
|
|
69
|
+
{% endfor %}
|
|
70
|
+
{% endfor %}
|
|
71
|
+
{% endif %}
|
|
72
|
+
</div>
|
|
73
|
+
{% endfor %}
|
|
74
|
+
{% endif %}
|
|
39
75
|
</div>
|
|
40
76
|
{% endif %}
|
|
41
77
|
</div>
|
|
@@ -276,12 +276,17 @@
|
|
|
276
276
|
repeatKeys.forEach(repeat_index => {
|
|
277
277
|
const arr = data[repeat_index][selectedKey];
|
|
278
278
|
if (arr && arr.length) {
|
|
279
|
-
arr.
|
|
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
|
+
|
|
280
285
|
if (typeof d === 'object' && d.x !== undefined && d.y !== undefined) {
|
|
281
|
-
x.push(
|
|
286
|
+
x.push(xVal);
|
|
282
287
|
y.push(d.y);
|
|
283
288
|
} else if (typeof d === 'number') {
|
|
284
|
-
x.push(
|
|
289
|
+
x.push(xVal);
|
|
285
290
|
y.push(d);
|
|
286
291
|
}
|
|
287
292
|
});
|
|
@@ -293,12 +298,16 @@
|
|
|
293
298
|
y: y,
|
|
294
299
|
mode: 'markers',
|
|
295
300
|
name: selectedKey,
|
|
301
|
+
marker: { size: 8 },
|
|
302
|
+
line: { shape: 'linear', width: 1 },
|
|
296
303
|
};
|
|
297
304
|
|
|
298
305
|
const layout = {
|
|
299
306
|
xaxis: {
|
|
300
|
-
title: '
|
|
301
|
-
gridcolor: '#e9ecef'
|
|
307
|
+
title: 'Trial (Batch Sub-index)',
|
|
308
|
+
gridcolor: '#e9ecef',
|
|
309
|
+
tickvals: repeatKeys.map(k => parseFloat(k)),
|
|
310
|
+
ticktext: repeatKeys.map(k => `Trial ${k}`),
|
|
302
311
|
},
|
|
303
312
|
yaxis: {
|
|
304
313
|
title: selectedKey,
|
ivoryos/routes/design/design.py
CHANGED
|
@@ -100,6 +100,27 @@ def experiment_builder():
|
|
|
100
100
|
script=script, defined_variables=deck_variables, buttons_dict=design_buttons,
|
|
101
101
|
local_variables=global_config.defined_variables, block_variables=global_config.building_blocks)
|
|
102
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)
|
|
123
|
+
|
|
103
124
|
|
|
104
125
|
@design.route("/draft/meta", methods=["PATCH"])
|
|
105
126
|
@login_required
|
|
@@ -320,13 +341,16 @@ def methods_handler(instrument: str = ''):
|
|
|
320
341
|
request.form
|
|
321
342
|
if "hidden_name" in request.form:
|
|
322
343
|
deck_snapshot = global_config.deck_snapshot
|
|
344
|
+
block_snapshot = global_config.building_blocks
|
|
323
345
|
method_name = request.form.get("hidden_name", None)
|
|
324
346
|
form = forms.get(method_name) if forms else None
|
|
325
347
|
insert_position = request.form.get("drop_target_id", None)
|
|
348
|
+
|
|
326
349
|
if form:
|
|
327
350
|
kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
|
|
328
351
|
if form.validate_on_submit():
|
|
329
352
|
function_name = kwargs.pop("hidden_name")
|
|
353
|
+
batch_action = kwargs.pop("batch_action", False)
|
|
330
354
|
save_data = kwargs.pop('return', '')
|
|
331
355
|
primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
|
|
332
356
|
|
|
@@ -335,11 +359,17 @@ def methods_handler(instrument: str = ''):
|
|
|
335
359
|
|
|
336
360
|
script.eval_list(kwargs, primitive_arg_types)
|
|
337
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)
|
|
338
367
|
action = {"instrument": instrument, "action": function_name,
|
|
339
368
|
"args": kwargs,
|
|
340
369
|
"return": save_data,
|
|
341
370
|
'arg_types': primitive_arg_types,
|
|
342
|
-
"coroutine":
|
|
371
|
+
"coroutine": coroutine,
|
|
372
|
+
"batch_action": batch_action,
|
|
343
373
|
}
|
|
344
374
|
script.add_action(action=action, insert_position=insert_position)
|
|
345
375
|
else:
|
|
@@ -57,8 +57,9 @@ def save_step(uuid: int):
|
|
|
57
57
|
kwargs = {field.name: field.data for field in forms if field.name != 'csrf_token'}
|
|
58
58
|
if forms and forms.validate_on_submit():
|
|
59
59
|
save_as = kwargs.pop('return', '')
|
|
60
|
+
batch_action = kwargs.pop('batch_action', False)
|
|
60
61
|
kwargs = script.validate_variables(kwargs)
|
|
61
|
-
script.update_by_uuid(uuid=uuid, args=kwargs, output=save_as)
|
|
62
|
+
script.update_by_uuid(uuid=uuid, args=kwargs, output=save_as, batch_action=batch_action)
|
|
62
63
|
else:
|
|
63
64
|
warning = f"Compilation failed: {str(forms.errors)}"
|
|
64
65
|
utils.post_script_file(script)
|