PyAres 1.2.0__tar.gz → 1.3.0__tar.gz
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.
- {pyares-1.2.0 → pyares-1.3.0}/PKG-INFO +3 -3
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Analyzing/__init__.py +2 -1
- pyares-1.3.0/PyAres/Analyzing/analysis_service.py +242 -0
- pyares-1.3.0/PyAres/Analyzing/analyzer_models.py +248 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Analyzers/analyzer_wiki.py +6 -4
- pyares-1.3.0/PyAres/Demo/Analyzers/ax_analyzer_test.py +80 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Planners/planner_test.py +5 -5
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Planning/planner_models.py +69 -17
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Planning/planning_service.py +61 -19
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_data_schema_utils.py +29 -11
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_service_base.py +62 -56
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/device_status_code_utils.py +1 -1
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/__init__.py +1 -0
- {pyares-1.2.0 → pyares-1.3.0}/pyproject.toml +1 -1
- {pyares-1.2.0 → pyares-1.3.0}/tests/test_analyzer.py +131 -4
- {pyares-1.2.0 → pyares-1.3.0}/tests/test_analyzer_integration.py +4 -1
- {pyares-1.2.0 → pyares-1.3.0}/tests/test_ares_data_schema_utils.py +29 -11
- pyares-1.3.0/tests/test_planner_analysis_data.py +182 -0
- pyares-1.2.0/PyAres/Analyzing/analysis_service.py +0 -147
- pyares-1.2.0/PyAres/Analyzing/analyzer_models.py +0 -66
- {pyares-1.2.0 → pyares-1.3.0}/.github/dependabot.yml +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/.github/workflows/pypi.yaml +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/.github/workflows/python-tests.yaml +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/.gitignore +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/LICENSE +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Analyzers/airship_analyzer.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Analyzers/analyzer_test.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Analyzers/analyzer_test_tools_demo.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Devices/device_test.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Devices/failure_test_device.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Devices/hotplate.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Devices/random_number_device.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Devices/rotary_mixer.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Planners/airship_planner.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Planners/planner_test_tools_demo.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Planners/planner_wiki.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Device/__init__.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Device/device_models.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Device/device_service.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Device/device_warnings.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Models/__init__.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Models/ares_data_models.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Planning/__init__.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_data_type_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_device_command_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_objective_status_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_outcome_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_plan_status_code_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_struct_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_value_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/datamodel_version_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/grpc_base.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/logging_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/metadata_interceptors.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/param_history_info_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/plan_request_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/plan_response_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/planning_param_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/test_tools/__init__.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/test_tools/analyzer_tester.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/PyAres/test_tools/planner_tester.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/README.md +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/setup.cfg +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/setup.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/tests/__init__.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/tests/mock_grpc_context.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/tests/test_ares_data_type_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/tests/test_ares_device.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/tests/test_ares_struct_utils.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/tests/test_ares_value_type_conversions.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/tests/test_batch_planning_regressions.py +0 -0
- {pyares-1.2.0 → pyares-1.3.0}/tests/test_planner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: PyAres
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: The official Python library for ARES.
|
|
5
5
|
Project-URL: Homepage, https://github.com/AFRL-ARES/PyAres
|
|
6
6
|
Author-email: AFRL <AFRL.RX.AREScontact@us.af.mil>
|
|
@@ -9,7 +9,7 @@ License-File: LICENSE
|
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
|
-
Requires-Dist: ares-datamodel>=0.
|
|
12
|
+
Requires-Dist: ares-datamodel>=0.36.0
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
|
|
15
15
|
# PyAres
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from .analysis_service import AresAnalyzerService
|
|
2
|
-
from .analyzer_models import AnalysisResponse, AnalysisRequest, InfoResponse
|
|
2
|
+
from .analyzer_models import AnalysisResponse, AnalysisRequest, InfoResponse, Objective
|
|
3
3
|
|
|
4
4
|
__all__ = [
|
|
5
5
|
"AnalysisResponse",
|
|
6
6
|
"AnalysisRequest",
|
|
7
7
|
"InfoResponse",
|
|
8
8
|
"AresAnalyzerService",
|
|
9
|
+
"Objective"
|
|
9
10
|
]
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Standard Imports
|
|
2
|
+
import grpc
|
|
3
|
+
from typing import Callable, Awaitable, Union, Mapping, Dict, Optional, List
|
|
4
|
+
|
|
5
|
+
# Import generated protobuf and gRPC stubs
|
|
6
|
+
from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2 as analyzer_service
|
|
7
|
+
from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2_grpc as analyzer_service_grpc
|
|
8
|
+
from ares_datamodel.analyzing import analysis_pb2
|
|
9
|
+
from ares_datamodel.analyzing import analyzer_capabilities_pb2
|
|
10
|
+
from ares_datamodel import ares_data_schema_pb2
|
|
11
|
+
from ares_datamodel import ares_outcome_enum_pb2
|
|
12
|
+
|
|
13
|
+
# Import Utilities
|
|
14
|
+
from ..Utils import ares_struct_utils
|
|
15
|
+
from ..Utils import ares_data_schema_utils
|
|
16
|
+
from ..Utils import ares_outcome_utils
|
|
17
|
+
from ..Utils import ares_value_utils
|
|
18
|
+
from ..Utils.ares_service_base import AresServiceWrapperBase, AresBaseService
|
|
19
|
+
|
|
20
|
+
# Import python models
|
|
21
|
+
from ..Models import ares_data_models, RequestMetadata, AresSchemaEntry
|
|
22
|
+
from .analyzer_models import AnalysisRequest, AnalysisResponse, ObjectiveSchema
|
|
23
|
+
|
|
24
|
+
# Type hints for the user's custom logic
|
|
25
|
+
AnalyzeLogicFunction = Callable[[AnalysisRequest], Union[AnalysisResponse, Awaitable[AnalysisResponse]]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AresAnalyzerServiceWrapper(AresServiceWrapperBase, analyzer_service_grpc.AresRemoteAnalyzerServiceServicer):
|
|
29
|
+
"""A wrapper around the gRPC service to expose native Python objects for analysis."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, name: str, version: str, description: str, timeout: int, custom_analysis_logic: AnalyzeLogicFunction):
|
|
32
|
+
super().__init__(name, version, description, timeout)
|
|
33
|
+
self._custom_analysis_logic = custom_analysis_logic
|
|
34
|
+
self._analysis_parameters: Dict[str, ares_data_schema_pb2.AresValueSchema] = {}
|
|
35
|
+
self._objective_outputs: Dict[str, ares_data_schema_pb2.AresValueSchema] = {}
|
|
36
|
+
|
|
37
|
+
def Analyze(self, request: analyzer_service.AnalysisRequest, context) -> analysis_pb2.AnalysisResponse:
|
|
38
|
+
print("Received an analysis request!")
|
|
39
|
+
try:
|
|
40
|
+
python_request = AnalysisRequest(
|
|
41
|
+
inputs=ares_struct_utils.ares_struct_to_dict(request.inputs),
|
|
42
|
+
settings=ares_struct_utils.ares_struct_to_dict(request.settings),
|
|
43
|
+
metadata=RequestMetadata(request.metadata),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
python_response = self._custom_analysis_logic(python_request)
|
|
47
|
+
python_response = self._resolve_awaitable(python_response)
|
|
48
|
+
|
|
49
|
+
if not isinstance(python_response, AnalysisResponse):
|
|
50
|
+
print("Analysis response was an invalid type.")
|
|
51
|
+
proto_response = analysis_pb2.AnalysisResponse()
|
|
52
|
+
proto_response.analysis_outcome = ares_outcome_enum_pb2.FAILURE
|
|
53
|
+
proto_response.error_string = (
|
|
54
|
+
"The user's custom analysis logic returned an invalid type; "
|
|
55
|
+
"expected AnalysisResponse."
|
|
56
|
+
)
|
|
57
|
+
return proto_response
|
|
58
|
+
|
|
59
|
+
if python_response.deprecated_result_usage:
|
|
60
|
+
print(
|
|
61
|
+
"WARNING: AnalysisResponse(result=...) usage is deprecated and will be "
|
|
62
|
+
"removed in a future major version. Please construct objectives explicitly."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
print("Sending AnalysisResponse.....")
|
|
66
|
+
proto_response = analysis_pb2.AnalysisResponse()
|
|
67
|
+
|
|
68
|
+
for obj in python_response.objectives:
|
|
69
|
+
obj_proto = analysis_pb2.Objective()
|
|
70
|
+
obj_proto.objective_name = obj.objective_name
|
|
71
|
+
|
|
72
|
+
ares_value_utils.py_to_ares_value(obj.objective_value, obj_proto.objective_value)
|
|
73
|
+
|
|
74
|
+
if obj.objective_metadata:
|
|
75
|
+
ares_struct_utils.dict_to_ares_struct(
|
|
76
|
+
obj.objective_metadata, obj_proto.objective_metadata
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
proto_response.objectives.append(obj_proto)
|
|
80
|
+
|
|
81
|
+
proto_response.analysis_outcome = ares_outcome_utils.python_ares_outcome_to_proto_ares_outcome(
|
|
82
|
+
python_response.outcome
|
|
83
|
+
)
|
|
84
|
+
proto_response.error_string = python_response.error_string
|
|
85
|
+
|
|
86
|
+
return proto_response
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
context.set_code(grpc.StatusCode.INTERNAL)
|
|
90
|
+
context.set_details(f"Error in custom analysis logic: {e}")
|
|
91
|
+
proto_response = analysis_pb2.AnalysisResponse()
|
|
92
|
+
proto_response.analysis_outcome = ares_outcome_enum_pb2.FAILURE
|
|
93
|
+
proto_response.error_string = str(e)
|
|
94
|
+
return proto_response
|
|
95
|
+
|
|
96
|
+
def GetAnalysisParameters(self, request, context):
|
|
97
|
+
print("Analysis Parameters Requested")
|
|
98
|
+
try:
|
|
99
|
+
analysis_param_response = analyzer_service.AnalysisParametersResponse()
|
|
100
|
+
|
|
101
|
+
for key, value in self._analysis_parameters.items():
|
|
102
|
+
map_entry = analysis_param_response.parameter_schema.fields[key]
|
|
103
|
+
map_entry.CopyFrom(value)
|
|
104
|
+
|
|
105
|
+
return analysis_param_response
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
print(f"Exception while trying to respond to ARES with analysis parameters! {e}")
|
|
109
|
+
|
|
110
|
+
def GetAnalyzerCapabilities(self, request, context) -> analyzer_capabilities_pb2.AnalyzerCapabilities:
|
|
111
|
+
print("Capabilities Requested!")
|
|
112
|
+
capabilities = analyzer_capabilities_pb2.AnalyzerCapabilities(timeout_seconds=self._timeout)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
for key, value in self._settings.items():
|
|
116
|
+
settings_entry = capabilities.settings_schema.fields[key]
|
|
117
|
+
settings_entry.CopyFrom(value)
|
|
118
|
+
|
|
119
|
+
for key, value in self._objective_outputs.items():
|
|
120
|
+
objective_entry = capabilities.objective_output_schema.fields[key]
|
|
121
|
+
objective_entry.CopyFrom(value)
|
|
122
|
+
|
|
123
|
+
return capabilities
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
print(f"Exception while trying to respond to ARES capabilities request! {e}")
|
|
127
|
+
return capabilities
|
|
128
|
+
|
|
129
|
+
def ValidateInputs(self, request: analyzer_service.ParameterValidationRequest, context):
|
|
130
|
+
response = analyzer_service.ParameterValidationResult(success=True)
|
|
131
|
+
provided_params: Mapping[str, ares_data_schema_pb2.AresValueSchema] = request.input_schema.fields
|
|
132
|
+
|
|
133
|
+
for stored_key, stored_schema in self._analysis_parameters.items():
|
|
134
|
+
if stored_key in provided_params:
|
|
135
|
+
matching_schema = provided_params[stored_key]
|
|
136
|
+
if stored_schema.type != matching_schema.type:
|
|
137
|
+
message = (
|
|
138
|
+
f"Schema Mismatch! {stored_key} was provided with the value type "
|
|
139
|
+
f"{stored_schema.type}, but the value type {matching_schema} was expected!"
|
|
140
|
+
)
|
|
141
|
+
response.messages.append(message)
|
|
142
|
+
else:
|
|
143
|
+
if not stored_schema.optional:
|
|
144
|
+
message = (
|
|
145
|
+
f"Schema Missing! {stored_key} is marked as a required piece of data for analysis, "
|
|
146
|
+
f"but no assignment was found in the provided schema!"
|
|
147
|
+
)
|
|
148
|
+
response.messages.append(message)
|
|
149
|
+
|
|
150
|
+
if len(response.messages) != 0:
|
|
151
|
+
response.success = False
|
|
152
|
+
|
|
153
|
+
return response
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class AresAnalyzerService(AresBaseService):
|
|
157
|
+
"""Manages the gRPC server for the AresAnalyzerService."""
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
custom_analysis_logic: AnalyzeLogicFunction,
|
|
162
|
+
name: str,
|
|
163
|
+
version: str,
|
|
164
|
+
description: str = "",
|
|
165
|
+
timeout: int = 30,
|
|
166
|
+
use_localhost: bool = True,
|
|
167
|
+
port: int = 7083,
|
|
168
|
+
max_message_size: int = -1,
|
|
169
|
+
):
|
|
170
|
+
"""Initializes the AresAnalyzerService."""
|
|
171
|
+
super().__init__(
|
|
172
|
+
service_name=name,
|
|
173
|
+
description=description,
|
|
174
|
+
version=version,
|
|
175
|
+
port=port,
|
|
176
|
+
use_localhost=use_localhost,
|
|
177
|
+
max_message_size=max_message_size,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
self._service_wrapper = AresAnalyzerServiceWrapper(
|
|
181
|
+
name=name,
|
|
182
|
+
version=version,
|
|
183
|
+
description=description,
|
|
184
|
+
timeout=timeout,
|
|
185
|
+
custom_analysis_logic=custom_analysis_logic,
|
|
186
|
+
)
|
|
187
|
+
analyzer_service_grpc.add_AresRemoteAnalyzerServiceServicer_to_server(
|
|
188
|
+
self._service_wrapper, self.get_server()
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def add_analysis_parameter(
|
|
192
|
+
self,
|
|
193
|
+
parameter_name: str,
|
|
194
|
+
parameter_type: ares_data_models.AresDataType,
|
|
195
|
+
optional: bool = False,
|
|
196
|
+
struct_schema: Optional[Dict[str, AresSchemaEntry]] = None,
|
|
197
|
+
list_element_schema: Optional[AresSchemaEntry] = None,
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Adds an analysis parameter that will be reported to ARES.
|
|
200
|
+
|
|
201
|
+
If `parameter_type` is LIST, `list_element_schema` can be used to describe
|
|
202
|
+
the shape of each element in that list.
|
|
203
|
+
"""
|
|
204
|
+
self._service_wrapper._analysis_parameters[parameter_name] = ares_data_schema_utils.create_settings_schema_entry(
|
|
205
|
+
setting_type=parameter_type,
|
|
206
|
+
optional=optional,
|
|
207
|
+
choices=[],
|
|
208
|
+
struct_schema=struct_schema,
|
|
209
|
+
list_element_schema=list_element_schema,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def add_objective_output(
|
|
213
|
+
self,
|
|
214
|
+
objective_name: str,
|
|
215
|
+
objective_type: ares_data_models.AresDataType,
|
|
216
|
+
objective_description: str = "",
|
|
217
|
+
optional: bool = False,
|
|
218
|
+
struct_schema: Optional[Dict[str, AresSchemaEntry]] = None,
|
|
219
|
+
list_element_schema: Optional[AresSchemaEntry] = None,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Adds an analysis objective to the advertised outputs of this analyzer.
|
|
222
|
+
|
|
223
|
+
If `objective_type` is LIST, `list_element_schema` can be used to describe
|
|
224
|
+
the schema of each element in that list.
|
|
225
|
+
"""
|
|
226
|
+
self._service_wrapper._objective_outputs[objective_name] = ares_data_schema_utils.create_settings_schema_entry(
|
|
227
|
+
setting_type=objective_type,
|
|
228
|
+
optional=optional,
|
|
229
|
+
choices=[],
|
|
230
|
+
struct_schema=struct_schema,
|
|
231
|
+
list_element_schema=list_element_schema,
|
|
232
|
+
description=objective_description,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def get_objective_schema(self) -> List[ObjectiveSchema]:
|
|
236
|
+
"""Return a fresh Python representation of the configured objective schema."""
|
|
237
|
+
schemas: List[ObjectiveSchema] = []
|
|
238
|
+
for name, proto_schema in self._service_wrapper._objective_outputs.items():
|
|
239
|
+
schemas.append(ObjectiveSchema(name, proto_schema))
|
|
240
|
+
|
|
241
|
+
return schemas
|
|
242
|
+
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
from typing import Dict, Any, List, Optional
|
|
2
|
+
import warnings
|
|
3
|
+
from ..Models import Outcome, RequestMetadata, AresDataType, AresSchemaEntry
|
|
4
|
+
from ares_datamodel import ares_data_schema_pb2
|
|
5
|
+
from ..Utils import ares_data_type_utils
|
|
6
|
+
|
|
7
|
+
class AnalysisRequest:
|
|
8
|
+
""" Represents an analysis request received from ARES. """
|
|
9
|
+
|
|
10
|
+
def __init__(self, inputs: Dict[str, Any], settings: Dict[str, Any], metadata: RequestMetadata):
|
|
11
|
+
self.inputs = inputs
|
|
12
|
+
self.settings = settings
|
|
13
|
+
self.request_metadata = metadata
|
|
14
|
+
|
|
15
|
+
def __str__(self) -> str:
|
|
16
|
+
"""Returns a string representation of the AnalysisRequest with all information organized."""
|
|
17
|
+
metadata_str = str(self.request_metadata).replace('\n', '\n\t')
|
|
18
|
+
return (f"AnalysisRequest object with:\n"
|
|
19
|
+
f" inputs: {self.inputs}\n"
|
|
20
|
+
f" settings: {self.settings}\n"
|
|
21
|
+
f" metadata: {metadata_str}")
|
|
22
|
+
|
|
23
|
+
def __repr__(self) -> str:
|
|
24
|
+
return self.__str__()
|
|
25
|
+
|
|
26
|
+
class Objective:
|
|
27
|
+
""" Represents a single analysis objective in the new AnalysisResponse model. """
|
|
28
|
+
|
|
29
|
+
def __init__(self, objective_name: str, objective_value: Any, objective_metadata: Optional[Dict[str, Any]] = None):
|
|
30
|
+
"""
|
|
31
|
+
Initializes a new Objective.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
objective_name: A name the user wants associated with this objective.
|
|
35
|
+
objective_value: The value associated with this objective, as calculated by the analyzer.
|
|
36
|
+
objective_metadata: Optional metadata associated with this objective, represented as a dictionary.
|
|
37
|
+
"""
|
|
38
|
+
self.objective_name = objective_name
|
|
39
|
+
self.objective_value = objective_value
|
|
40
|
+
self.objective_metadata = objective_metadata or {}
|
|
41
|
+
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
return (f"Objective object with:\n"
|
|
44
|
+
f" objective_name: {self.objective_name}\n"
|
|
45
|
+
f" objective_value: {self.objective_value}\n"
|
|
46
|
+
f" objective_metadata: {self.objective_metadata}")
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
return self.__str__()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ObjectiveSchema:
|
|
53
|
+
"""A schema representing the expected form of an objective."""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
objective_name: str,
|
|
58
|
+
proto_objective_schema: ares_data_schema_pb2.AresValueSchema,
|
|
59
|
+
):
|
|
60
|
+
self.objective_name = objective_name
|
|
61
|
+
# Convert basic scalar fields
|
|
62
|
+
self.objective_type: AresDataType = ares_data_type_utils.proto_ares_type_to_python_ares_type(proto_objective_schema.type)
|
|
63
|
+
self.objective_description: str = proto_objective_schema.description
|
|
64
|
+
self.optional: bool = proto_objective_schema.optional
|
|
65
|
+
|
|
66
|
+
# Convert nested struct schema, if present
|
|
67
|
+
self.struct_schema: Optional[Dict[str, AresSchemaEntry]] = None
|
|
68
|
+
if proto_objective_schema.struct_schema.fields:
|
|
69
|
+
self.struct_schema = {}
|
|
70
|
+
for field_name, field_schema in proto_objective_schema.struct_schema.fields.items():
|
|
71
|
+
self.struct_schema[field_name] = self._proto_value_schema_to_ares_schema_entry(field_schema)
|
|
72
|
+
|
|
73
|
+
# Convert list element schema, if present
|
|
74
|
+
self.list_element_schema: Optional[AresSchemaEntry] = None
|
|
75
|
+
# Treat type UNKNOWN (0) as "no list element schema configured"
|
|
76
|
+
if proto_objective_schema.list_element_schema.type != 0:
|
|
77
|
+
self.list_element_schema = self._proto_value_schema_to_ares_schema_entry(
|
|
78
|
+
proto_objective_schema.list_element_schema
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _proto_value_schema_to_ares_schema_entry(
|
|
83
|
+
proto: ares_data_schema_pb2.AresValueSchema,
|
|
84
|
+
) -> AresSchemaEntry:
|
|
85
|
+
"""Convert a proto AresValueSchema into a Python AresSchemaEntry."""
|
|
86
|
+
py_type = ares_data_type_utils.proto_ares_type_to_python_ares_type(proto.type)
|
|
87
|
+
|
|
88
|
+
# Extract choices if present
|
|
89
|
+
choices: List[Any] = []
|
|
90
|
+
if proto.string_choices.strings:
|
|
91
|
+
choices = list(proto.string_choices.strings)
|
|
92
|
+
elif proto.number_choices.numbers:
|
|
93
|
+
choices = list(proto.number_choices.numbers)
|
|
94
|
+
|
|
95
|
+
entry = AresSchemaEntry(
|
|
96
|
+
type=py_type,
|
|
97
|
+
optional=proto.optional,
|
|
98
|
+
description=proto.description,
|
|
99
|
+
choices=choices,
|
|
100
|
+
struct_schema=None,
|
|
101
|
+
list_element_schema=None,
|
|
102
|
+
min_number_value=getattr(proto, "min_number_value", None),
|
|
103
|
+
max_number_value=getattr(proto, "max_number_value", None),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Nested struct schema
|
|
107
|
+
if proto.struct_schema.fields:
|
|
108
|
+
entry.struct_schema = {}
|
|
109
|
+
for field_name, field_schema in proto.struct_schema.fields.items():
|
|
110
|
+
entry.struct_schema[field_name] = ObjectiveSchema._proto_value_schema_to_ares_schema_entry(
|
|
111
|
+
field_schema
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Nested list element schema
|
|
115
|
+
if proto.list_element_schema.type != 0:
|
|
116
|
+
entry.list_element_schema = ObjectiveSchema._proto_value_schema_to_ares_schema_entry(
|
|
117
|
+
proto.list_element_schema
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return entry
|
|
121
|
+
|
|
122
|
+
def __str__(self) -> str:
|
|
123
|
+
struct_keys = list(self.struct_schema.keys()) if self.struct_schema else []
|
|
124
|
+
list_elem_type = (
|
|
125
|
+
self.list_element_schema.type.name
|
|
126
|
+
if self.list_element_schema and hasattr(self.list_element_schema.type, "name")
|
|
127
|
+
else None
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
f"ObjectiveSchema(\n"
|
|
132
|
+
f" objective_name={self.objective_name!r},\n"
|
|
133
|
+
f" objective_type={self.objective_type.name if hasattr(self.objective_type, 'name') else self.objective_type},\n"
|
|
134
|
+
f" objective_description={self.objective_description!r},\n"
|
|
135
|
+
f" optional={self.optional},\n"
|
|
136
|
+
f" struct_schema_keys={struct_keys},\n"
|
|
137
|
+
f" list_element_type={list_elem_type}\n"
|
|
138
|
+
f")"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def __repr__(self) -> str:
|
|
142
|
+
return self.__str__()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class AnalysisResponse:
|
|
146
|
+
""" Represents the result of an analysis process using objectives.
|
|
147
|
+
|
|
148
|
+
Preferred usage:
|
|
149
|
+
- Construct with a list of Objective instances:
|
|
150
|
+
AnalysisResponse(
|
|
151
|
+
objectives=[
|
|
152
|
+
Objective("primary_metric", 0.87, {"units": "accuracy"})
|
|
153
|
+
],
|
|
154
|
+
outcome=Outcome.SUCCESS,
|
|
155
|
+
error_string=""
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
Deprecated usage:
|
|
159
|
+
- Construct with a single scalar result (will be removed in a future major version):
|
|
160
|
+
AnalysisResponse(result=0.87)
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
objectives: Optional[List[Objective]] = None,
|
|
166
|
+
outcome: Outcome = Outcome.SUCCESS,
|
|
167
|
+
error_string: str = "",
|
|
168
|
+
result: Optional[float] = None,
|
|
169
|
+
):
|
|
170
|
+
"""
|
|
171
|
+
Initializes an AnalysisResponse message.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
objectives: A list of Objective instances representing the objectives returned by analysis.
|
|
175
|
+
outcome: An Outcome value that represents whether analysis was done successfully.
|
|
176
|
+
error_string: An optional string argument for passing why analysis failed to ARES.
|
|
177
|
+
result: DEPRECATED. A single numeric result that will be wrapped into a default Objective.
|
|
178
|
+
This parameter is deprecated and will be removed in a future major version.
|
|
179
|
+
"""
|
|
180
|
+
self._deprecated_result_usage = False
|
|
181
|
+
|
|
182
|
+
if objectives is not None and result is not None:
|
|
183
|
+
raise ValueError("AnalysisResponse cannot be constructed with both 'objectives' and deprecated 'result'. "
|
|
184
|
+
"Use 'objectives' only.")
|
|
185
|
+
|
|
186
|
+
if objectives is None and result is not None:
|
|
187
|
+
# Deprecated path: single scalar result
|
|
188
|
+
warnings.warn(
|
|
189
|
+
"AnalysisResponse(result=...) is deprecated; use "
|
|
190
|
+
"AnalysisResponse(objectives=[Objective(...)]) instead. "
|
|
191
|
+
"Support will be removed in a future major version.",
|
|
192
|
+
DeprecationWarning
|
|
193
|
+
)
|
|
194
|
+
self._deprecated_result_usage = True
|
|
195
|
+
|
|
196
|
+
# Use a default objective name for backward compatibility
|
|
197
|
+
default_objective = Objective(
|
|
198
|
+
objective_name="result",
|
|
199
|
+
objective_value=result,
|
|
200
|
+
objective_metadata=None,
|
|
201
|
+
)
|
|
202
|
+
self.objectives: List[Objective] = [default_objective]
|
|
203
|
+
elif objectives is not None:
|
|
204
|
+
self.objectives = objectives
|
|
205
|
+
else:
|
|
206
|
+
# No objectives and no result provided: treat as empty objectives
|
|
207
|
+
self.objectives = []
|
|
208
|
+
|
|
209
|
+
self.outcome = outcome
|
|
210
|
+
self.error_string = error_string
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def deprecated_result_usage(self) -> bool:
|
|
214
|
+
"""Indicates whether this AnalysisResponse was created via the deprecated 'result' parameter."""
|
|
215
|
+
return self._deprecated_result_usage
|
|
216
|
+
|
|
217
|
+
def __str__(self) -> str:
|
|
218
|
+
if not self.objectives:
|
|
219
|
+
objectives_str = "\t- (none)"
|
|
220
|
+
|
|
221
|
+
else:
|
|
222
|
+
objectives_str = "\n".join(
|
|
223
|
+
[f"\t- {obj.objective_name}: value={obj.objective_value}, metadata={obj.objective_metadata}"
|
|
224
|
+
for obj in self.objectives])
|
|
225
|
+
|
|
226
|
+
return (f"AnalysisResponse object with:\n"
|
|
227
|
+
f" objectives:\n{objectives_str}\n"
|
|
228
|
+
f" outcome: {self.outcome}\n"
|
|
229
|
+
f" error_string: {self.error_string}")
|
|
230
|
+
|
|
231
|
+
def __repr__(self) -> str:
|
|
232
|
+
return self.__str__()
|
|
233
|
+
|
|
234
|
+
class InfoResponse:
|
|
235
|
+
""" A response message that provides basic information about your analyzer. """
|
|
236
|
+
|
|
237
|
+
def __init__(self, name: str, version: str, description: str = ""):
|
|
238
|
+
"""Initializes a new InfoResponse message.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
name: The name of your analyzer, to be displayed in ARES.
|
|
242
|
+
version: The specific version of your analyzer. This information is saved in experiment results that use your analyzer.
|
|
243
|
+
description: An optional (but recommended) string that gives a basic description of your analyzer.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
self.name = name
|
|
247
|
+
self.version = version
|
|
248
|
+
self.description = description
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from PyAres import
|
|
1
|
+
from PyAres import *
|
|
2
2
|
|
|
3
3
|
def analyze_sample(request: AnalysisRequest) -> AnalysisResponse:
|
|
4
4
|
# 1. Extract inputs
|
|
@@ -6,15 +6,17 @@ def analyze_sample(request: AnalysisRequest) -> AnalysisResponse:
|
|
|
6
6
|
raw_value = request.inputs.get("Growth_Metric")
|
|
7
7
|
|
|
8
8
|
if raw_value is None:
|
|
9
|
-
return AnalysisResponse(
|
|
9
|
+
return AnalysisResponse(objectives=[], outcome=Outcome.FAILURE, error_string="No raw value provided, cannot analyze")
|
|
10
10
|
|
|
11
11
|
# 2. Perform Logic
|
|
12
12
|
print(f"Analyzing sample with value: {raw_value}")
|
|
13
13
|
|
|
14
|
-
calculated_score = raw_value * 1.5
|
|
14
|
+
calculated_score = raw_value * 1.5
|
|
15
|
+
|
|
16
|
+
objective_score = Objective("Calculated Score", calculated_score)
|
|
15
17
|
|
|
16
18
|
# 3. Return Result
|
|
17
|
-
return AnalysisResponse(
|
|
19
|
+
return AnalysisResponse(objectives=[objective_score], outcome=Outcome.SUCCESS)
|
|
18
20
|
|
|
19
21
|
if __name__ == "__main__":
|
|
20
22
|
service = AresAnalyzerService(
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from PyAres import *
|
|
2
|
+
import math
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
def analyze(request: AnalysisRequest) -> AnalysisResponse:
|
|
6
|
+
"""
|
|
7
|
+
Evaluates the parameters chosen by your AX Planner and returns a simulated yield.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
# 1. Extract the values ARES OS logged for this specific iteration.
|
|
12
|
+
# Ensure the parameter names in your ARES Campaign match these strings exactly.
|
|
13
|
+
temperature = request.inputs["Temperature"]
|
|
14
|
+
concentration = request.inputs["Concentration"]
|
|
15
|
+
|
|
16
|
+
# 2. Define the "Hidden Ground Truth"
|
|
17
|
+
# This is the goal your AX Planner is trying to discover.
|
|
18
|
+
ideal_temp = 165.0
|
|
19
|
+
ideal_conc = 3.2
|
|
20
|
+
max_yield = 100.0
|
|
21
|
+
|
|
22
|
+
# 3. Calculate the distance penalty (the variances control the "width" of the peak)
|
|
23
|
+
temp_variance = 400.0
|
|
24
|
+
conc_variance = 2.0
|
|
25
|
+
|
|
26
|
+
distance_penalty = (((temperature - ideal_temp) ** 2) / temp_variance) + \
|
|
27
|
+
(((concentration - ideal_conc) ** 2) / conc_variance)
|
|
28
|
+
|
|
29
|
+
# 4. Calculate theoretical yield and add noise
|
|
30
|
+
simulated_yield = max_yield * math.exp(-distance_penalty)
|
|
31
|
+
noise = random.gauss(0, 1.5) # mean=0, std_dev=1.5
|
|
32
|
+
|
|
33
|
+
# Clamp the final yield between 0 and 100%
|
|
34
|
+
final_yield = max(0.0, min(100.0, simulated_yield + noise))
|
|
35
|
+
|
|
36
|
+
print(f"[Demo Analyzer] Received T={temperature:.1f}, C={concentration:.1f} | Calculated Yield: {final_yield:.2f}%")
|
|
37
|
+
|
|
38
|
+
# Preferred new usage: return an objective-based AnalysisResponse
|
|
39
|
+
return AnalysisResponse(
|
|
40
|
+
objectives=[
|
|
41
|
+
Objective(
|
|
42
|
+
objective_name="yield",
|
|
43
|
+
objective_value=final_yield,
|
|
44
|
+
objective_metadata={"units": "%"}
|
|
45
|
+
)
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print(f"[Demo Analyzer] Error during analysis: {e}")
|
|
51
|
+
# If extraction fails (e.g., missing parameter names), return a terrible score so the planner learns to avoid it
|
|
52
|
+
return AnalysisResponse(
|
|
53
|
+
objectives=[
|
|
54
|
+
Objective(
|
|
55
|
+
objective_name="yield",
|
|
56
|
+
objective_value=None,
|
|
57
|
+
objective_metadata={"error": str(e)}
|
|
58
|
+
)
|
|
59
|
+
],
|
|
60
|
+
outcome=Outcome.FAILURE,
|
|
61
|
+
error_string=str(e),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
# Initialize the Analyzer Service
|
|
68
|
+
demo_analyzer = AresAnalyzerService(custom_analysis_logic=analyze,
|
|
69
|
+
name="Simulated Yield Demo",
|
|
70
|
+
version="1.0.0",
|
|
71
|
+
description="Calculates a simulated material yield based on a hidden ideal Temperature (165) and Concentration (3.2).",
|
|
72
|
+
port=8200)
|
|
73
|
+
|
|
74
|
+
demo_analyzer.add_analysis_parameter("Temperature", AresDataType.NUMBER)
|
|
75
|
+
demo_analyzer.add_analysis_parameter("Concentration", AresDataType.NUMBER)
|
|
76
|
+
|
|
77
|
+
demo_analyzer.add_objective_output("yield", AresDataType.NUMBER, "A numeric value that indicates the experiments yielded result")
|
|
78
|
+
|
|
79
|
+
print("Starting PyAres Simulated Goal Analyzer...")
|
|
80
|
+
demo_analyzer.start()
|
|
@@ -8,12 +8,12 @@ def plan(request: PlanRequest) -> PlanResponse:
|
|
|
8
8
|
gpdoods = []
|
|
9
9
|
names = []
|
|
10
10
|
|
|
11
|
-
for
|
|
12
|
-
|
|
11
|
+
for objective_set in request.analysis_objectives:
|
|
12
|
+
print(f"Received a total of {len(request.analysis_objectives)} objective sets")
|
|
13
13
|
|
|
14
|
-
for
|
|
15
|
-
|
|
16
|
-
print(f"
|
|
14
|
+
for objective in objective_set:
|
|
15
|
+
print(f"Objective Name: {objective.objective_name}")
|
|
16
|
+
print(f"Objective Value: {objective.objective_value}")
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
for param in request.parameters:
|