ivoryos 1.2.1__py3-none-any.whl → 1.2.2__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.

@@ -0,0 +1,164 @@
1
+ # optimizers/ax_optimizer.py
2
+ from typing import Dict
3
+
4
+
5
+
6
+ from ivoryos.optimizer.base_optimizer import OptimizerBase
7
+ from ivoryos.utils.utils import install_and_import
8
+
9
+ class AxOptimizer(OptimizerBase):
10
+ def __init__(self, experiment_name, parameter_space, objective_config, optimizer_config=None):
11
+ try:
12
+ from ax.api.client import Client
13
+ except ImportError as e:
14
+ install_and_import("ax", "ax-platform")
15
+ 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)
17
+
18
+ self.client = Client()
19
+ # 2. Configure where Ax will search.
20
+ self.client.configure_experiment(
21
+ name=experiment_name,
22
+ parameters=self._convert_parameter_to_ax_format(parameter_space)
23
+ )
24
+ # 3. Configure the objective function.
25
+ self.client.configure_optimization(objective=self._convert_objective_to_ax_format(objective_config))
26
+ if optimizer_config:
27
+ self.client.set_generation_strategy(self._convert_generator_to_ax_format(optimizer_config))
28
+ self.generators = self._create_generator_mapping()
29
+
30
+ @staticmethod
31
+ def _create_generator_mapping():
32
+ """Create a mapping from string values to Generator enum members."""
33
+ from ax.modelbridge import Generators
34
+ return {member.value: member for member in Generators}
35
+
36
+ def _convert_parameter_to_ax_format(self, parameter_space):
37
+ """
38
+ Converts the parameter space configuration to Baybe format.
39
+ :param parameter_space: The parameter space configuration.
40
+ [
41
+ {"name": "param_1", "type": "range", "bounds": [1.0, 2.0], "value_type": "float"},
42
+ {"name": "param_2", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"},
43
+ {"name": "param_3", "type": "range", "bounds": [0 10], "value_type": "int"},
44
+ ]
45
+ :return: A list of Baybe parameters.
46
+ """
47
+ from ax import RangeParameterConfig, ChoiceParameterConfig
48
+ ax_params = []
49
+ for p in parameter_space:
50
+ if p["type"] == "range":
51
+ ax_params.append(
52
+ RangeParameterConfig(
53
+ name=p["name"],
54
+ bounds=tuple(p["bounds"]),
55
+ parameter_type=p["value_type"]
56
+ ))
57
+ elif p["type"] == "choice":
58
+ ax_params.append(
59
+ ChoiceParameterConfig(
60
+ name=p["name"],
61
+ values=p["bounds"],
62
+ parameter_type=p["value_type"],
63
+ )
64
+ )
65
+ return ax_params
66
+
67
+ def _convert_objective_to_ax_format(self, objective_config: list):
68
+ """
69
+ Converts the objective configuration to Baybe format.
70
+ :param parameter_space: The parameter space configuration.
71
+ [
72
+ {"name": "obj_1", "minimize": True, "weight": 1},
73
+ {"name": "obj_2", "minimize": False, "weight": 2}
74
+ ]
75
+ :return: Ax objective configuration. "-cost, utility"
76
+ """
77
+ objectives = []
78
+ for obj in objective_config:
79
+ obj_name = obj.get("name")
80
+ minimize = obj.get("minimize", True)
81
+ weight = obj.get("weight", 1)
82
+ sign = "-" if minimize else ""
83
+ objectives.append(f"{sign}{weight} * {obj_name}")
84
+ return ", ".join(objectives)
85
+
86
+ def _convert_generator_to_ax_format(self, optimizer_config):
87
+ """
88
+ Converts the optimizer configuration to Ax format.
89
+ :param optimizer_config: The optimizer configuration.
90
+ :return: Ax generator configuration.
91
+ """
92
+ from ax.generation_strategy.generation_node import GenerationStep
93
+ from ax.generation_strategy.generation_strategy import GenerationStrategy
94
+ generators = self._create_generator_mapping()
95
+ step_1 = optimizer_config.get("step_1", {})
96
+ step_2 = optimizer_config.get("step_2", {})
97
+ step_1_generator = step_1.get("model", "Sobol")
98
+ step_2_generator = step_2.get("model", "BOTorch")
99
+ generator_1 = GenerationStep(model=generators.get(step_1_generator), num_trials=step_1.get("num_samples", 5))
100
+ generator_2 = GenerationStep(model=generators.get(step_2_generator), num_trials=step_2.get("num_samples", -1))
101
+ return GenerationStrategy(steps=[generator_1, generator_2])
102
+
103
+ def suggest(self, n=1):
104
+ trial_index, params = self.client.get_next_trials(1).popitem()
105
+ self.trial_index = trial_index
106
+ return params
107
+
108
+ def observe(self, results):
109
+ self.client.complete_trial(
110
+ trial_index=self.trial_index,
111
+ raw_data=results
112
+ )
113
+
114
+ @staticmethod
115
+ def get_schema():
116
+ return {
117
+ "parameter_types": ["range", "choice"],
118
+ "multiple_objectives": True,
119
+ # "objective_weights": True,
120
+ "optimizer_config": {
121
+ "step_1": {"model": ["Sobol", "Uniform", "Factorial", "Thompson"], "num_samples": 5},
122
+ "step_2": {"model": ["BoTorch", "SAASBO", "SAAS_MTGP", "Legacy_GPEI", "EB", "EB_Ashr", "ST_MTGP", "BO_MIXED", "Contextual_SACBO"]}
123
+ },
124
+ }
125
+
126
+ def append_existing_data(self, existing_data):
127
+ """
128
+ Append existing data to the Ax experiment.
129
+ :param existing_data: A dictionary containing existing data.
130
+ """
131
+ from pandas import DataFrame
132
+ if not existing_data:
133
+ return
134
+ if isinstance(existing_data, DataFrame):
135
+ existing_data = existing_data.to_dict(orient="records")
136
+ parameter_names = [i.get("name") for i in self.parameter_space]
137
+ objective_names = [i.get("name") for i in self.objective_config]
138
+ for name, value in existing_data.items():
139
+ # First attach the trial and note the trial index
140
+ parameters = {name: value for name in existing_data if name in parameter_names}
141
+ trial_index = self.client.attach_trial(parameters=parameters)
142
+ raw_data = {name: value for name in existing_data if name in objective_names}
143
+ # Then complete the trial with the existing data
144
+ self.client.complete_trial(trial_index=trial_index, raw_data=raw_data)
145
+
146
+
147
+ if __name__ == "__main__":
148
+ # Example usage
149
+ optimizer = AxOptimizer(
150
+ experiment_name="example_experiment",
151
+ parameter_space=[
152
+ {"name": "param_1", "type": "range", "bounds": [0.0, 1.0], "value_type": "float"},
153
+ {"name": "param_2", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"}
154
+ ],
155
+ objective_config=[
156
+ {"name": "objective_1", "minimize": True},
157
+ {"name": "objective_2", "minimize": False}
158
+ ],
159
+ optimizer_config={
160
+ "step_1": {"model": "Sobol", "num_samples": 5},
161
+ "step_2": {"model": "BoTorch"}
162
+ }
163
+ )
164
+ print(optimizer._create_generator_mapping())
@@ -0,0 +1,65 @@
1
+ ### ivoryos/optimizers/base.py
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+
7
+ class OptimizerBase(ABC):
8
+ def __init__(self, experiment_name:str, parameter_space: list, objective_config: dict, optimizer_config: dict):
9
+ """
10
+ :param experiment_name: arbitrary name
11
+ :param parameter_space: list of parameter names
12
+ [
13
+ {"name": "param_1", "type": "range", "bounds": [1.0, 2.0], "value_type": "float"},
14
+ {"name": "param_2", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"},
15
+ {"name": "param_3", "type": "range", "bounds": [0 10], "value_type": "int"},
16
+ ]
17
+ :param objective_config: objective configuration
18
+ [
19
+ {"name": "obj_1", "minimize": True, "weight": 1},
20
+ {"name": "obj_2", "minimize": False, "weight": 1}
21
+ ]
22
+ :param optimizer_config: optimizer configuration
23
+ optimizer_config={
24
+ "step_1": {"model": "Random", "num_samples": 10},
25
+ "step_2": {"model": "BOTorch"}
26
+ }
27
+ """
28
+ self.experiment_name = experiment_name
29
+ self.parameter_space = parameter_space
30
+ self.objective_config = objective_config
31
+ self.optimizer_config = optimizer_config
32
+
33
+ @abstractmethod
34
+ def suggest(self, n=1):
35
+ pass
36
+
37
+ @abstractmethod
38
+ def observe(self, results: dict):
39
+ """
40
+ observe
41
+ :param results: {"objective_name": "value"}
42
+ """
43
+ pass
44
+
45
+ @abstractmethod
46
+ def append_existing_data(self, existing_data):
47
+ pass
48
+
49
+ @staticmethod
50
+ def get_schema():
51
+ """
52
+ Returns a template for the optimizer configuration.
53
+ """
54
+ return {
55
+ "parameter_types": ["range", "choice"],
56
+ "multiple_objectives": True,
57
+ # "objective_weights": True,
58
+ "optimizer_config": {
59
+ "step_1": {"model": [], "num_samples": 10},
60
+ "step_2": {"model": []}
61
+ },
62
+ }
63
+
64
+
65
+
@@ -0,0 +1,183 @@
1
+ ### Directory: ivoryos/optimizers/baybe_optimizer.py
2
+ from typing import Dict
3
+
4
+
5
+ from ivoryos.utils.utils import install_and_import
6
+ from ivoryos.optimizer.base_optimizer import OptimizerBase
7
+
8
+ class BaybeOptimizer(OptimizerBase):
9
+ def __init__(self, experiment_name, parameter_space, objective_config, optimizer_config):
10
+ try:
11
+ from baybe import Campaign
12
+ except ImportError:
13
+ install_and_import("baybe")
14
+ print("Please install Baybe with pip install baybe to before register BaybeOptimizer.")
15
+
16
+ super().__init__(experiment_name, parameter_space, objective_config, optimizer_config)
17
+ self._trial_id = 0
18
+ self._trials = {}
19
+
20
+ self.experiment = Campaign(
21
+ searchspace=self._convert_parameter_to_searchspace(parameter_space),
22
+ objective=self._convert_objective_to_baybe_format(objective_config),
23
+ recommender=self._convert_recommender_to_baybe_format(optimizer_config),
24
+ )
25
+
26
+
27
+ def suggest(self, n=1):
28
+ self.df = self.experiment.recommend(batch_size=n)
29
+ return self.df.to_dict(orient="records")[0]
30
+
31
+ def observe(self, results, index=None):
32
+ """
33
+ Observes the results of a trial and updates the experiment.
34
+ :param results: A dictionary containing the results of the trial.
35
+ :param index: The index of the trial in the DataFrame, if applicable.
36
+
37
+ """
38
+ for name, value in results.items():
39
+ self.df[name] = [value]
40
+ self.experiment.add_measurements(self.df)
41
+
42
+ def append_existing_data(self, existing_data: Dict):
43
+ """
44
+ Append existing data to the Ax experiment.
45
+ :param existing_data: A dictionary containing existing data.
46
+ """
47
+ import pandas as pd
48
+ if not existing_data:
49
+ return
50
+ # parameter_names = [i.get("name") for i in self.parameter_space]
51
+ # objective_names = [i.get("name") for i in self.objective_config]
52
+ self.experiment.add_measurements(pd.DataFrame(existing_data))
53
+ # for name, value in existing_data.items():
54
+ # # First attach the trial and note the trial index
55
+ # parameters = {name: value for name in existing_data if name in parameter_names}
56
+ # trial_index = self.client.attach_trial(parameters=parameters)
57
+ # raw_data = {name: value for name in existing_data if name in objective_names}
58
+ # # Then complete the trial with the existing data
59
+ # self.client.complete_trial(trial_index=trial_index, raw_data=raw_data)
60
+
61
+
62
+ def _convert_parameter_to_searchspace(self, parameter_space):
63
+ """
64
+ Converts the parameter space configuration to Baybe format.
65
+ :param parameter_space: The parameter space configuration.
66
+ [
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
71
+ ]
72
+ :return: A list of Baybe parameters.
73
+ """
74
+ from baybe.parameters.categorical import CategoricalParameter
75
+ from baybe.parameters.numerical import NumericalContinuousParameter, NumericalDiscreteParameter
76
+ from baybe.searchspace import SearchSpace
77
+ parameters = []
78
+ for p in parameter_space:
79
+ if p["type"] == "range":
80
+ if p["value_type"] == "float":
81
+ parameters.append(NumericalContinuousParameter(name=p["name"], bounds=p["bounds"]))
82
+ elif p["value_type"] == "int":
83
+ values = tuple([int(v) for v in range(p["bounds"][0], p["bounds"][1] + 1)])
84
+ parameters.append(NumericalDiscreteParameter(name=p["name"], values=values))
85
+
86
+ elif p["type"] == "choice":
87
+ if p["value_type"] == "str":
88
+ parameters.append(CategoricalParameter(name=p["name"], values=p["bounds"]))
89
+ elif p["value_type"] in ["int", "float"]:
90
+ parameters.append(NumericalDiscreteParameter(name=p["name"], values=p["bounds"]))
91
+ return SearchSpace.from_product(parameters)
92
+
93
+ def _convert_objective_to_baybe_format(self, objective_config):
94
+ """
95
+ Converts the objective configuration to Baybe format.
96
+ :param parameter_space: The parameter space configuration.
97
+ [
98
+ {"name": "obj_1", "minimize": True},
99
+ {"name": "obj_2", "minimize": False}
100
+ ]
101
+ :return: A Baybe objective configuration.
102
+ """
103
+ from baybe.targets import NumericalTarget
104
+ from baybe.objectives import SingleTargetObjective, DesirabilityObjective, ParetoObjective
105
+ targets = []
106
+ weights = []
107
+ for obj in objective_config:
108
+ obj_name = obj.get("name")
109
+ minimize = obj.get("minimize", False)
110
+ weight = obj.get("weight", 1)
111
+ weights.append(weight)
112
+ targets.append(NumericalTarget(name=obj_name, mode="MAX" if minimize else "MIN"))
113
+
114
+ if len(targets) == 1:
115
+ return SingleTargetObjective(target=targets[0])
116
+ else:
117
+ # Handle multiple objectives
118
+ return ParetoObjective(targets=targets)
119
+
120
+
121
+ def _convert_recommender_to_baybe_format(self, recommender_config):
122
+ """
123
+ Converts the recommender configuration to Baybe format.
124
+ :param recommender_config: The recommender configuration.
125
+ :return: A Baybe recommender configuration.
126
+ """
127
+ from baybe.recommenders import (
128
+ BotorchRecommender,
129
+ FPSRecommender,
130
+ TwoPhaseMetaRecommender,
131
+ RandomRecommender,
132
+ NaiveHybridSpaceRecommender
133
+ )
134
+ step_1 = recommender_config.get("step_1", {})
135
+ step_2 = recommender_config.get("step_2", {})
136
+ step_1_recommender = step_1.get("model", "Random")
137
+ step_2_recommender = step_2.get("model", "BOTorch")
138
+ if step_1.get("model") == "Random":
139
+ step_1_recommender = RandomRecommender()
140
+ elif step_1.get("model") == "FPS":
141
+ step_1_recommender = FPSRecommender()
142
+ if step_2.get("model") == "Naive Hybrid Space":
143
+ step_2_recommender = NaiveHybridSpaceRecommender()
144
+ elif step_2.get("model") == "BOTorch":
145
+ step_2_recommender = BotorchRecommender()
146
+ return TwoPhaseMetaRecommender(
147
+ initial_recommender=step_1_recommender,
148
+ recommender=step_2_recommender
149
+ )
150
+
151
+ @staticmethod
152
+ def get_schema():
153
+ """
154
+ Returns a template for the optimizer configuration.
155
+ """
156
+ return {
157
+ "parameter_types": ["range", "choice", "substance"],
158
+ "multiple_objectives": True,
159
+ "optimizer_config": {
160
+ "step_1": {"model": ["Random", "FPS"], "num_samples": 10},
161
+ "step_2": {"model": ["BOTorch", "Naive Hybrid Space"]}
162
+ },
163
+ }
164
+
165
+ if __name__ == "__main__":
166
+ # Example usage
167
+ baybe_optimizer = BaybeOptimizer(
168
+ experiment_name="example_experiment",
169
+ parameter_space=[
170
+ {"name": "param_1", "type": "range", "bounds": [1.0, 2.0], "value_type": "float"},
171
+ {"name": "param_2", "type": "choice", "bounds": ["a", "b", "c"], "value_type": "str"},
172
+ {"name": "param_3", "type": "range", "bounds": [0, 10], "value_type": "int"}
173
+ ],
174
+ objective_config=[
175
+ {"name": "obj_1", "minimize": True},
176
+ {"name": "obj_2", "minimize": False}
177
+ ],
178
+ optimizer_config={
179
+ "step_1": {"model": "Random", "num_samples": 10},
180
+ "step_2": {"model": "BOTorch"}
181
+ }
182
+ )
183
+ print(baybe_optimizer.suggest(5))
@@ -0,0 +1,9 @@
1
+ # optimizers/registry.py
2
+
3
+ from ivoryos.optimizer.ax_optimizer import AxOptimizer
4
+ from ivoryos.optimizer.baybe_optimizer import BaybeOptimizer
5
+
6
+ OPTIMIZER_REGISTRY = {
7
+ "ax": AxOptimizer,
8
+ "baybe": BaybeOptimizer
9
+ }
@@ -0,0 +1,56 @@
1
+ import os
2
+ from flask import Blueprint, jsonify, request, current_app
3
+
4
+ from ivoryos.routes.control.control import find_instrument_by_name
5
+ from ivoryos.utils.form import create_form_from_module
6
+ from ivoryos.utils.global_config import GlobalConfig
7
+ from ivoryos.utils.db_models import Script, WorkflowRun, SingleStep, WorkflowStep
8
+
9
+ from ivoryos.socket_handlers import abort_pending, abort_current, pause, retry, runner
10
+ from ivoryos.utils.task_runner import TaskRunner
11
+
12
+ api = Blueprint('api', __name__)
13
+ global_config = GlobalConfig()
14
+ task_runner = TaskRunner()
15
+
16
+ #TODO: add authentication and authorization to the API endpoints
17
+
18
+
19
+ @api.route("/control/", strict_slashes=False, methods=['GET'])
20
+ @api.route("/control/<string:instrument>", methods=['POST'])
21
+ def backend_control(instrument: str=None):
22
+ """
23
+ .. :quickref: Backend Control; backend control
24
+
25
+ backend control through http requests
26
+
27
+ .. http:get:: /api/control/
28
+
29
+ :param instrument: instrument name
30
+ :type instrument: str
31
+
32
+ .. http:post:: /api/control/
33
+
34
+ """
35
+ if instrument:
36
+ inst_object = find_instrument_by_name(instrument)
37
+ forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
38
+
39
+ if request.method == 'POST':
40
+ method_name = request.form.get("hidden_name", None)
41
+ form = forms.get(method_name, None)
42
+ if form:
43
+ kwargs = {field.name: field.data for field in form if field.name not in ['csrf_token', 'hidden_name']}
44
+ wait = request.form.get("hidden_wait", "true") == "true"
45
+ output = task_runner.run_single_step(component=instrument, method=method_name, kwargs=kwargs, wait=wait,
46
+ current_app=current_app._get_current_object())
47
+ return jsonify(output), 200
48
+
49
+ snapshot = global_config.deck_snapshot.copy()
50
+ # Iterate through each instrument in the snapshot
51
+ for instrument_key, instrument_data in snapshot.items():
52
+ # Iterate through each function associated with the current instrument
53
+ for function_key, function_data in instrument_data.items():
54
+ # Convert the function signature to a string representation
55
+ function_data['signature'] = str(function_data['signature'])
56
+ return jsonify(snapshot), 200
ivoryos/utils/serilize.py CHANGED
@@ -7,8 +7,8 @@ import sys
7
7
 
8
8
  import flask
9
9
 
10
- from example.abstract_sdl_example.abstract_balance import AbstractBalance
11
- from example.abstract_sdl_example.abstract_pump import AbstractPump
10
+ from example.abstract_sdl_example import abstract_sdl as deck
11
+
12
12
 
13
13
 
14
14
  class ScriptAnalyzer:
@@ -179,14 +179,12 @@ class ScriptAnalyzer:
179
179
  print(f" {name}: {error}")
180
180
 
181
181
  if __name__ == "__main__":
182
- balance = AbstractBalance("re")
183
- pump = AbstractPump("re")
184
182
 
185
183
  _analyzer = ScriptAnalyzer()
186
- module = sys.modules[__name__]
184
+ # module = sys.modules[deck]
187
185
  try:
188
186
 
189
- result, with_warnings, failed, _ = _analyzer.analyze_module(module)
187
+ result, with_warnings, failed, _ = _analyzer.analyze_module(deck)
190
188
 
191
189
  output_path = f"analysis.json"
192
190
  _analyzer.save_to_json(result, output_path)
ivoryos/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.2.1"
1
+ __version__ = "1.2.2"
@@ -1,11 +1,11 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
- Home-page: https://gitlab.com/heingroup/ivoryos
6
- Author: Ivory Zhang
7
- Author-email: ivoryzhang@chem.ubc.ca
5
+ Author-email: Ivory Zhang <ivoryzhang@chem.ubc.ca>
8
6
  License: MIT
7
+ Project-URL: Homepage, https://gitlab.com/heingroup/ivoryos
8
+ Requires-Python: >=3.7
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: bcrypt
@@ -17,6 +17,10 @@ Requires-Dist: Flask-WTF
17
17
  Requires-Dist: SQLAlchemy-Utils
18
18
  Requires-Dist: python-dotenv
19
19
  Requires-Dist: astor; python_version < "3.9"
20
+ Provides-Extra: optimizer
21
+ Requires-Dist: ax-platform; extra == "optimizer"
22
+ Requires-Dist: baybe; extra == "optimizer"
23
+ Dynamic: license-file
20
24
 
21
25
  [![Documentation Status](https://readthedocs.org/projects/ivoryos/badge/?version=latest)](https://ivoryos.readthedocs.io/en/latest/?badge=latest)
22
26
  [![PyPI version](https://img.shields.io/pypi/v/ivoryos)](https://pypi.org/project/ivoryos/)
@@ -1,8 +1,13 @@
1
1
  ivoryos/__init__.py,sha256=x4PnDTbhx1ZTugfuclGQ03GO3yidAfiUFU7vuGyKD8M,9830
2
2
  ivoryos/config.py,sha256=y3RxNjiIola9tK7jg-mHM8EzLMwiLwOzoisXkDvj0gA,2174
3
3
  ivoryos/socket_handlers.py,sha256=VWVWiIdm4jYAutwGu6R0t1nK5MuMyOCL0xAnFn06jWQ,1302
4
- ivoryos/version.py,sha256=Mlm4Gvmb_6yQxwUbv2Ksc-BJFXLPg9H1Vt2iV7wXrA4,22
4
+ ivoryos/version.py,sha256=uuf4VNtTNA93fMhoAur9YafzaKJFnczY-H1SSCSuRVQ,22
5
+ ivoryos/optimizer/ax_optimizer.py,sha256=PoSu8hrDFFpqyhRBnaSMswIUsDfEX6sPWt8NEZ_sobs,7112
6
+ ivoryos/optimizer/base_optimizer.py,sha256=JTbUharZKn0t8_BDbAFuwZIbT1VOnX1Xuog1pJuU8hY,1992
7
+ ivoryos/optimizer/baybe_optimizer.py,sha256=EdrrRiYO-IOx610cPXiQhH4qG8knUP0uiZ0YoyaGIU8,7954
8
+ ivoryos/optimizer/registry.py,sha256=lr0cqdI2iEjw227ZPRpVkvsdYdddjeJJRzawDv77cEc,219
5
9
  ivoryos/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ ivoryos/routes/api/api.py,sha256=X_aZMB_nCxW41pqZpJOiEEwGmlqLqJUArruevuy41v0,2284
6
11
  ivoryos/routes/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
12
  ivoryos/routes/auth/auth.py,sha256=CqoP9cM8BuXVGHGujX7-0sNAOdWILU9amyBrObOD6Ss,3283
8
13
  ivoryos/routes/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -32,20 +37,11 @@ ivoryos/utils/global_config.py,sha256=zNO9GYhGn7El3msWoxJIm3S4Mzb3VMh2i5ZEsVtvb2
32
37
  ivoryos/utils/llm_agent.py,sha256=-lVCkjPlpLues9sNTmaT7bT4sdhWvV2DiojNwzB2Lcw,6422
33
38
  ivoryos/utils/py_to_json.py,sha256=fyqjaxDHPh-sahgT6IHSn34ktwf6y51_x1qvhbNlH-U,7314
34
39
  ivoryos/utils/script_runner.py,sha256=g3_pLYcu6gF9sPjhW9WRlwMH7ScDpz_MqMJzxNayfyg,16725
35
- ivoryos/utils/serilize.py,sha256=Ded3Vbicl4KYN8GJ_gY7HJMuInAeFZEPk5ZtuEtHPns,6933
40
+ ivoryos/utils/serilize.py,sha256=lkBhkz8r2bLmz2_xOb0c4ptSSOqjIu6krj5YYK4Nvj8,6784
36
41
  ivoryos/utils/task_runner.py,sha256=cDIcmDaqYh0vXoYaL_kO877pluAo2tyfsHl9OgZqJJE,3029
37
42
  ivoryos/utils/utils.py,sha256=-WiU0_brszB9yDsiQepf_7SzNgPTSpul2RSKDOY3pqo,13921
38
- tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- tests/conftest.py,sha256=u2sQ6U-Lghyl7et1Oz6J2E5VZ47VINKcjRM_2leAE2s,3627
40
- tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
- tests/integration/test_route_auth.py,sha256=l3ZDqr0oiCWS3yYSXGK5yMP6qI2t7Sv5I9zoYTkiyQU,2754
42
- tests/integration/test_route_control.py,sha256=YYIll84bTUEKiAxFiFSz6LF3fTldPNfCtHs0IR3mSdM,3935
43
- tests/integration/test_route_database.py,sha256=mS026W_hEuCTMpSkdRWvM-f4MYykK_6nRDJ4K5a7QA0,2342
44
- tests/integration/test_route_design.py,sha256=PJAvGRiCY6B53Pu1v5vPAVHHsuaqRmRKk2eesSNshLU,1157
45
- tests/integration/test_route_main.py,sha256=bmuf8Y_9CRWhiLLf4up11ltEd5YCdsLx6I-o26VGDEw,1228
46
- tests/integration/test_sockets.py,sha256=4ZyFyExm7a-DYzVqpzEONpWeb1a0IT68wyFaQu0rY_Y,925
47
- ivoryos-1.2.1.dist-info/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
48
- ivoryos-1.2.1.dist-info/METADATA,sha256=AiQqfIiig_KOYF5ths0jGNMbax1YV44deM6wKwHwnAY,9247
49
- ivoryos-1.2.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
50
- ivoryos-1.2.1.dist-info/top_level.txt,sha256=mIOiZkdpSwxFJt1R5fsyOff8mNprXHq1nMGNKNULIyE,14
51
- ivoryos-1.2.1.dist-info/RECORD,,
43
+ ivoryos-1.2.2.dist-info/licenses/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
44
+ ivoryos-1.2.2.dist-info/METADATA,sha256=vvueQx-kWo57d7AvBCDtUp28Ot_DBmgeTaZ6HVNS3ec,9416
45
+ ivoryos-1.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
+ ivoryos-1.2.2.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
47
+ ivoryos-1.2.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.45.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
tests/__init__.py DELETED
File without changes
tests/conftest.py DELETED
@@ -1,133 +0,0 @@
1
- from enum import Enum
2
-
3
- import bcrypt
4
- import pytest
5
- from ivoryos.config import get_config
6
-
7
- from ivoryos import create_app, socketio, db as _db, utils, global_config
8
- from ivoryos.utils.db_models import User
9
-
10
-
11
- @pytest.fixture(scope='session')
12
- def app():
13
- """Create a new app instance for the test session."""
14
- _app = create_app(get_config('testing'))
15
- return _app
16
-
17
- @pytest.fixture
18
- def client(app):
19
- """A test client for the app."""
20
- with app.test_client() as client:
21
- with app.app_context():
22
- _db.create_all()
23
- yield client
24
- with app.app_context():
25
- _db.drop_all()
26
-
27
- # @pytest.fixture(scope='session')
28
- # def db(app):
29
- # """Session-wide test database."""
30
- # with app.app_context():
31
- # _db.create_all()
32
- # yield _db
33
- # _db.drop_all()
34
-
35
- @pytest.fixture(scope='module')
36
- def init_database(app):
37
- """
38
- Creates the database tables and seeds it with a default test user.
39
- This runs once per test module.
40
- """
41
- with app.app_context():
42
- # Drop everything first to ensure a clean slate
43
- _db.drop_all()
44
- # Create the database tables
45
- _db.create_all()
46
-
47
- # Insert a default user for authentication tests
48
- # Note: In a real app with password hashing, you'd call a hash function here.
49
- password = 'password'
50
- bcrypt_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
51
- default_user = User(username='testuser', password=bcrypt_password)
52
- _db.session.add(default_user)
53
- _db.session.commit()
54
-
55
- yield _db # this is where the testing happens!
56
-
57
- # Teardown: drop all tables after the tests in the module are done
58
- _db.drop_all()
59
-
60
-
61
- # ---------------------
62
- # Authentication Fixture
63
- # ---------------------
64
-
65
- @pytest.fixture(scope='function')
66
- def auth(client, init_database):
67
- """
68
- Logs in the default user for a single test function.
69
- Depends on `init_database` to ensure the user exists.
70
- Handles logout as part of teardown.
71
- """
72
- # Log in the default user
73
- client.post('/ivoryos/auth/login', data={
74
- 'username': 'testuser',
75
- 'password': 'password'
76
- }, follow_redirects=True)
77
-
78
- yield client # this is where the testing happens!
79
-
80
- # Log out the user after the test is done
81
- client.get('/ivoryos/auth/logout', follow_redirects=True)
82
-
83
-
84
- @pytest.fixture
85
- def socketio_client(app):
86
- """A test client for Socket.IO."""
87
- return socketio.test_client(app)
88
-
89
-
90
- class TestEnum(Enum):
91
- """An example Enum for testing type conversion."""
92
- OPTION_A = 'A'
93
- OPTION_B = 'B'
94
-
95
- class DummyModule:
96
- """A more comprehensive dummy instrument for testing."""
97
- def int_method(self, arg: int = 1):
98
- return arg
99
-
100
- def float_method(self, arg: float = 1.0):
101
- return arg
102
-
103
- def bool_method(self, arg: bool = False):
104
- return arg
105
-
106
- def list_method(self, arg: list = None):
107
- return arg or []
108
-
109
- def enum_method(self, arg: TestEnum = TestEnum.OPTION_A):
110
- return arg
111
-
112
- def str_method(self) -> dict:
113
- return {'status': 'OK'}
114
-
115
-
116
- @pytest.fixture
117
- def test_deck(app):
118
- """
119
- A fixture that creates and loads a predictable 'deck' of dummy instruments
120
- for testing the dynamic control routes.
121
- """
122
- dummy_module = DummyModule()
123
- snapshot = utils.create_deck_snapshot(dummy_module)
124
-
125
- with app.app_context():
126
- global_config.deck_snapshot = snapshot
127
- global_config.deck = dummy_module # instantiate the class
128
-
129
- yield DummyModule
130
-
131
- with app.app_context():
132
- global_config.deck_snapshot = {}
133
- global_config.deck = {}
File without changes
@@ -1,80 +0,0 @@
1
- from ivoryos.utils.db_models import User, db
2
-
3
-
4
- def test_get_signup(client):
5
- """
6
- GIVEN a client
7
- WHEN a GET request is made to /ivoryos/auth/signup
8
- THEN check that signup page loads with 200 status and contains "Signup" text
9
- """
10
- response = client.get("/ivoryos/auth/signup", follow_redirects=True)
11
- assert response.status_code == 200
12
- assert b"Signup" in response.data
13
-
14
-
15
- def test_route_auth_signup(client):
16
- """
17
- GIVEN a client
18
- WHEN a POST request is made to /ivoryos/auth/signup with valid credentials
19
- THEN check that signup succeeds with 200 status and the user is created in database
20
- """
21
- response = client.post("/ivoryos/auth/signup",
22
- data={
23
- "username": "second_testuser",
24
- "password": "password"
25
- },
26
- follow_redirects=True
27
- )
28
- assert response.status_code == 200
29
- assert b"Login" in response.data
30
-
31
- # Verify user was created
32
- with client.application.app_context():
33
- user = db.session.query(User).filter(User.username == 'second_testuser').first()
34
- assert user is not None
35
-
36
-
37
- def test_duplicate_user_signup(client, init_database):
38
- """
39
- GIVEN a client and init_database fixture
40
- WHEN a POST request is made to signup with an existing username
41
- THEN check that signup fails with 409 status and appropriate error message
42
- """
43
- client.post('/ivoryos/auth/signup', data={
44
- 'username': 'existinguser',
45
- 'password': 'anotherpass'
46
- })
47
- # Try to create duplicate
48
- response = client.post('/ivoryos/auth/signup', data={
49
- 'username': 'existinguser',
50
- 'password': 'anotherpass'
51
- })
52
- assert response.status_code == 409
53
- assert b"Signup" in response.data
54
- assert b"User already exists" in response.data
55
-
56
- # Verify user was created
57
- users = db.session.query(User).filter(User.username == 'existinguser').all()
58
- assert len(users) == 1
59
-
60
-
61
- def test_failed_login(client):
62
- """
63
- GIVEN a client and invalid login credentials
64
- WHEN a POST request is made to /ivoryos/auth/login
65
- THEN check that login fails with 401 status and the appropriate error message
66
- """
67
- response = client.post('/ivoryos/auth/login', data={
68
- 'username': 'nonexistent',
69
- 'password': 'wrongpass'
70
- })
71
- assert response.status_code == 401
72
-
73
- def test_logout(auth):
74
- """
75
- GIVEN an authenticated client
76
- WHEN a GET request is made to /ivoryos/auth/logout
77
- THEN check that logout succeeds with 302 status and redirects to login
78
- """
79
- response = auth.get('/ivoryos/auth/logout')
80
- assert response.status_code == 302 # Redirect to login
@@ -1,94 +0,0 @@
1
- from unittest.mock import patch, Mock
2
-
3
- from ivoryos.utils.db_models import Script
4
- from ivoryos import db
5
-
6
- def test_control_panel_redirects_anonymous(client):
7
- """
8
- GIVEN an anonymous user
9
- WHEN the control panel is accessed
10
- THEN they should be redirected to the login page
11
- """
12
- response = client.get('/ivoryos/control/home/deck', follow_redirects=True)
13
- assert response.status_code == 200
14
- assert b'Login' in response.data
15
-
16
- def test_deck_control_for_auth_user(auth):
17
- """
18
- GIVEN an authenticated user
19
- WHEN the control panel is accessed
20
- THEN the page should load successfully
21
- """
22
- response = auth.get('/ivoryos/control/home/deck', follow_redirects=True)
23
- assert response.status_code == 200
24
- assert b'<title>IvoryOS | Devices</title>' in response.data # Assuming this text exists on the page
25
-
26
- def test_temp_control_for_auth_user(auth):
27
- """
28
- GIVEN an authenticated user
29
- WHEN the control panel is accessed
30
- THEN the page should load successfully
31
- """
32
- response = auth.get('/ivoryos/control/home/temp', follow_redirects=True)
33
- assert response.status_code == 200
34
- # assert b'<title>IvoryOS | Devices</title>' in response.data # Assuming this text exists on the page
35
-
36
- def test_new_controller_page(auth):
37
- """Test new controller page loads"""
38
- response = auth.get('/ivoryos/control/new/')
39
- assert response.status_code == 200
40
-
41
- def test_download_proxy(self, auth_headers):
42
- """Test proxy download functionality"""
43
- with patch('ivoryos.routes.control.control.global_config') as mock_config:
44
- mock_config.deck_snapshot = {'test_instrument': {'test_method': {'signature': 'test()'}}}
45
- response = auth_headers.get('/ivoryos/control/download')
46
- assert response.status_code == 200
47
- assert response.headers['Content-Disposition'].startswith('attachment')
48
-
49
- def test_backend_control_get(self, auth_headers):
50
- """Test backend control GET endpoint"""
51
- with patch('ivoryos.routes.control.control.global_config') as mock_config:
52
- mock_config.deck_snapshot = {'test_instrument': {'test_method': {'signature': 'test()'}}}
53
- response = auth_headers.get('/ivoryos/api/control/')
54
- assert response.status_code == 200
55
- assert response.is_json
56
-
57
- @patch('ivoryos.routes.control.control.runner')
58
- @patch('ivoryos.routes.control.control.find_instrument_by_name')
59
- @patch('ivoryos.routes.control.control.create_form_from_module')
60
- def test_backend_control_post(self, mock_form, mock_find, mock_runner, auth_headers):
61
- """Test backend control POST endpoint"""
62
- # Setup mocks
63
- mock_instrument = Mock()
64
- mock_find.return_value = mock_instrument
65
- mock_field = Mock()
66
- mock_field.name = 'test_param'
67
- mock_field.data = 'test_value'
68
- mock_form_instance = Mock()
69
- mock_form_instance.__iter__ = Mock(return_value=iter([mock_field]))
70
- mock_form.return_value = {'test_method': mock_form_instance}
71
- mock_runner.run_single_step.return_value = 'success'
72
- response = auth_headers.post('/ivoryos/api/control/test_instrument', data={
73
- 'hidden_name': 'test_method',
74
- 'hidden_wait': 'true'
75
- })
76
- assert response.status_code == 200
77
-
78
- # def test_control(auth, app):
79
- # """
80
- # GIVEN an authenticated user and an existing script
81
- # WHEN a POST request is made to run the script
82
- # THEN the user should be redirected and a success message shown
83
- # """
84
- # # We need to create a script in the database first
85
- # with app.app_context():
86
- # script = Script(name='My Test Script', author='testuser', content='print("hello")')
87
- # db.session.add(script)
88
- # db.session.commit()
89
- # script_id = script.id
90
- #
91
- # # Simulate running the script
92
- # response = auth.post(f'/ivoryos/control/run/{script_id}', follow_redirects=True)
93
- # assert response.status_code == 200
94
- # assert b'has been initiated' in response.data # Check for a flash message
@@ -1,61 +0,0 @@
1
- from datetime import datetime
2
-
3
- from ivoryos.utils.db_models import Script, WorkflowRun, WorkflowStep
4
- from ivoryos import db
5
-
6
- def test_database_scripts_page(auth):
7
- """
8
- GIVEN an authenticated user
9
- WHEN they access the script database page
10
- THEN the page should load and show their scripts
11
- """
12
- # First, create a script so the page has something to render
13
- with auth.application.app_context():
14
- script = Script(name='test_script', author='testuser')
15
- db.session.add(script)
16
- db.session.commit()
17
-
18
- response = auth.get('/ivoryos/database/scripts/', follow_redirects=True)
19
- assert response.status_code == 200
20
- # assert b'Scripts Database' in response.data
21
- assert b'<title>IvoryOS | Design Database</title>' in response.data
22
-
23
- def test_database_workflows_page(auth):
24
- """
25
- GIVEN an authenticated user
26
- WHEN they access the workflow database page
27
- THEN the page should load and show past workflow runs
28
- """
29
- # Create a workflow run to display
30
- with auth.application.app_context():
31
- run = WorkflowRun(name="untitled", platform="deck",start_time=datetime.now())
32
- db.session.add(run)
33
- db.session.commit()
34
- run_id = run.id
35
-
36
- response = auth.get('/ivoryos/database/workflows/', follow_redirects=True)
37
- assert response.status_code == 200
38
- assert b'Workflow ID' in response.data
39
- # assert b'run_id' in response.data
40
-
41
- def test_view_specific_workflow(auth):
42
- """
43
- GIVEN an authenticated user and an existing workflow run
44
- WHEN they access the specific URL for that workflow
45
- THEN the detailed view for that run should be displayed
46
- """
47
- with auth.application.app_context():
48
- run = WorkflowRun(name='test_workflow', platform='test_platform', start_time=datetime.now())
49
- db.session.add(run)
50
- db.session.commit()
51
- run_id = run.id
52
-
53
- step = WorkflowStep(method_name='test_step', workflow_id=run_id, phase="main", run_error=False, start_time=datetime.now())
54
- db.session.add(step)
55
- db.session.commit()
56
- # run_id = run.id
57
-
58
- response = auth.get(f'/ivoryos/database/workflows/{run_id}', follow_redirects=True)
59
- assert response.status_code == 200
60
- # assert b'test_step' in response.data # Check for a title on the view page
61
- assert b'test_workflow' in response.data
@@ -1,36 +0,0 @@
1
- def test_design_page_loads_for_auth_user(auth):
2
- """
3
- GIVEN an authenticated user
4
- WHEN the design page is accessed
5
- THEN the page should load successfully
6
- """
7
- response = auth.get('/ivoryos/design/script/', follow_redirects=True)
8
- assert response.status_code == 200
9
- assert b'<title>IvoryOS | Design</title>' in response.data # Assuming this text exists
10
-
11
-
12
- def test_clear_canvas(auth):
13
- """
14
- Tests clearing the design canvas.
15
- """
16
- response = auth.get('/ivoryos/design/clear', follow_redirects=True)
17
- assert response.status_code == 200
18
- # assert b'Operations' in response.data
19
-
20
- # def test_add_action(auth, test_deck):
21
- # """
22
- # Tests adding an action to the design canvas.
23
- # """
24
- # response = auth.post('/ivoryos/design/script/deck.dummy/', data={
25
- # 'hidden_name': 'int_method',
26
- # 'arg': '10'
27
- # }, follow_redirects=True)
28
- # assert response.status_code == 200
29
-
30
- def test_experiment_run_page(auth):
31
- """
32
- Tests the experiment run page.
33
- """
34
- response = auth.get('/ivoryos/design/campaign')
35
- assert response.status_code == 200
36
- assert b'Run Panel' in response.data
@@ -1,35 +0,0 @@
1
- from flask_login import current_user
2
-
3
-
4
- def test_home_page_authenticated(auth, app):
5
- """
6
- GIVEN an authenticated user (using the 'auth' fixture)
7
- WHEN the home page is accessed
8
- THEN check that they see the main application page
9
- """
10
- with auth.application.test_request_context('/ivoryos/'):
11
- # Manually trigger the before_request functions that Flask-Login uses
12
- app.preprocess_request()
13
-
14
- # Assert that the `current_user` proxy is now populated and authenticated
15
- assert current_user.is_authenticated
16
- assert current_user.username == 'testuser'
17
-
18
- def test_help_page(client):
19
- """
20
- GIVEN an unauthenticated user
21
- WHEN they access the help page
22
- THEN check that the page loads successfully and contains documentation content
23
- """
24
- response = client.get('/ivoryos/help')
25
- assert response.status_code == 200
26
- assert b'Documentations' in response.data
27
-
28
- def test_prefix_redirect(auth):
29
- """
30
- GIVEN an authenticated user (using the 'auth' fixture)
31
- WHEN the home page is accessed without prefix
32
- THEN check that they see the main application page
33
- """
34
- response = auth.get('/', follow_redirects=True)
35
- assert response.status_code == 200
@@ -1,26 +0,0 @@
1
- def test_socket_connection(socketio_client):
2
- """
3
- Test that a client can successfully connect to the Socket.IO server.
4
- """
5
- assert socketio_client.is_connected()
6
- socketio_client.disconnect()
7
- assert not socketio_client.is_connected()
8
-
9
-
10
- # def test_logger_socket_event(socketio_client):
11
- # """
12
- # Test the custom logging event handler.
13
- # (This assumes you have a handler like `@socketio.on('start_log')`)
14
- # """
15
- # # Connect the client
16
- # socketio_client.connect()
17
- #
18
- # # Emit an event from the client to the server
19
- # socketio_client.emit('start_log', {'logger_name': 'my_test_logger'})
20
- #
21
- # # Check what the server sent back to the client
22
- # received = socketio_client.get_received()
23
- #
24
- # assert len(received) > 0
25
- # assert received[0]['name'] == 'log_message' # Check for the event name
26
- # assert 'Logger my_test_logger started' in received[0]['args'][0]['data']