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.
Files changed (75) hide show
  1. docs/source/conf.py +84 -0
  2. ivoryos/__init__.py +16 -246
  3. ivoryos/app.py +154 -0
  4. ivoryos/optimizer/ax_optimizer.py +55 -28
  5. ivoryos/optimizer/base_optimizer.py +20 -1
  6. ivoryos/optimizer/baybe_optimizer.py +27 -17
  7. ivoryos/optimizer/nimo_optimizer.py +173 -0
  8. ivoryos/optimizer/registry.py +3 -1
  9. ivoryos/routes/auth/auth.py +35 -8
  10. ivoryos/routes/auth/templates/change_password.html +32 -0
  11. ivoryos/routes/control/control.py +58 -28
  12. ivoryos/routes/control/control_file.py +12 -15
  13. ivoryos/routes/control/control_new_device.py +21 -11
  14. ivoryos/routes/control/templates/controllers.html +27 -0
  15. ivoryos/routes/control/utils.py +2 -0
  16. ivoryos/routes/data/data.py +110 -44
  17. ivoryos/routes/data/templates/components/step_card.html +78 -13
  18. ivoryos/routes/data/templates/workflow_view.html +343 -113
  19. ivoryos/routes/design/design.py +59 -10
  20. ivoryos/routes/design/design_file.py +3 -3
  21. ivoryos/routes/design/design_step.py +43 -17
  22. ivoryos/routes/design/templates/components/action_form.html +2 -2
  23. ivoryos/routes/design/templates/components/canvas_main.html +6 -1
  24. ivoryos/routes/design/templates/components/edit_action_form.html +18 -3
  25. ivoryos/routes/design/templates/components/info_modal.html +318 -0
  26. ivoryos/routes/design/templates/components/instruments_panel.html +23 -1
  27. ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
  28. ivoryos/routes/design/templates/experiment_builder.html +3 -0
  29. ivoryos/routes/execute/execute.py +82 -22
  30. ivoryos/routes/execute/templates/components/logging_panel.html +50 -25
  31. ivoryos/routes/execute/templates/components/run_tabs.html +45 -2
  32. ivoryos/routes/execute/templates/components/tab_bayesian.html +447 -325
  33. ivoryos/routes/execute/templates/components/tab_configuration.html +303 -18
  34. ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
  35. ivoryos/routes/execute/templates/experiment_run.html +0 -264
  36. ivoryos/routes/library/library.py +9 -11
  37. ivoryos/routes/main/main.py +30 -2
  38. ivoryos/server.py +180 -0
  39. ivoryos/socket_handlers.py +1 -1
  40. ivoryos/static/ivoryos_logo.png +0 -0
  41. ivoryos/static/js/action_handlers.js +259 -88
  42. ivoryos/static/js/socket_handler.js +40 -5
  43. ivoryos/static/js/sortable_design.js +29 -11
  44. ivoryos/templates/base.html +61 -2
  45. ivoryos/utils/bo_campaign.py +18 -17
  46. ivoryos/utils/client_proxy.py +267 -36
  47. ivoryos/utils/db_models.py +286 -60
  48. ivoryos/utils/decorators.py +34 -0
  49. ivoryos/utils/form.py +52 -19
  50. ivoryos/utils/global_config.py +21 -0
  51. ivoryos/utils/nest_script.py +314 -0
  52. ivoryos/utils/py_to_json.py +80 -10
  53. ivoryos/utils/script_runner.py +573 -189
  54. ivoryos/utils/task_runner.py +69 -22
  55. ivoryos/utils/utils.py +48 -5
  56. ivoryos/version.py +1 -1
  57. {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/METADATA +109 -47
  58. ivoryos-1.4.4.dist-info/RECORD +119 -0
  59. ivoryos-1.4.4.dist-info/top_level.txt +3 -0
  60. tests/__init__.py +0 -0
  61. tests/conftest.py +133 -0
  62. tests/integration/__init__.py +0 -0
  63. tests/integration/test_route_auth.py +80 -0
  64. tests/integration/test_route_control.py +94 -0
  65. tests/integration/test_route_database.py +61 -0
  66. tests/integration/test_route_design.py +36 -0
  67. tests/integration/test_route_main.py +35 -0
  68. tests/integration/test_sockets.py +26 -0
  69. tests/unit/test_type_conversion.py +42 -0
  70. tests/unit/test_util.py +3 -0
  71. ivoryos/routes/api/api.py +0 -56
  72. ivoryos-1.2.5.dist-info/RECORD +0 -100
  73. ivoryos-1.2.5.dist-info/top_level.txt +0 -1
  74. {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +0 -0
  75. {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.df.to_dict(orient="records")[0]
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
- for name, value in results.items():
39
- self.df[name] = [value]
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: Dict):
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
- import pandas as pd
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(pd.DataFrame(existing_data))
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": "choice", "bounds": ["a", "b", "c"], "value_type": "str"},
69
- {"name": "param_3", "type": "range", "bounds": [0 10], "value_type": "int"},
70
- {"name": "param_4", "type": "substance", "bounds": ["methanol", "water", "toluene"], "value_type": "str"} #TODO
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["value_type"] == "float":
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", False)
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, mode="MAX" if minimize else "MIN"))
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
+ )
@@ -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
  }
@@ -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
- deck_variables = global_config.deck_snapshot.keys()
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
- _forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
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(_forms.keys())
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: _forms[name] for name in order if name in _forms}
62
- # Handle POST for method execution
63
- if request.method == 'POST':
64
- all_kwargs = request.form.copy()
65
- method_name = all_kwargs.pop("hidden_name", None)
66
- form = forms.get(method_name)
67
- kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'} if form else {}
68
- if form and form.validate_on_submit():
69
- kwargs.pop("hidden_name", None)
70
- output = runner.run_single_step(instrument, method_name, kwargs, wait=True, current_app=current_app._get_current_object())
71
- if output["success"]:
72
- flash(f"\nRun Success! Output value: {output.get('output', 'None')}.")
73
- else:
74
- flash(f"\nRun Error! {output.get('output', 'Unknown error occurred.')}", "error")
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
- if form:
77
- flash(form.errors)
78
- else:
79
- flash("Invalid method selected.")
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
- 'controllers.html',
82
- defined_variables=deck_variables,
83
- temp_variables=temp_variables,
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 export_to_python, create_function
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; download proxy interface
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
- class_definitions = {}
25
- # Iterate through each instrument in the snapshot
26
- for instrument_key, instrument_data in snapshot.items():
27
- # Iterate through each function associated with the current instrument
28
- for function_key, function_data in instrument_data.items():
29
- # Convert the function signature to a string representation
30
- function_data['signature'] = str(function_data['signature'])
31
- class_name = instrument_key.split('.')[-1] # Extracting the class name from the path
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)